feat: Update .gitattributes and .gitignore for improved asset management and ignore patterns
chore: Add app.json and omoolab.css for Obsidian integration refactor: Clean up AGENTS.md and remove outdated test files
This commit is contained in:
132
.gitattributes
vendored
132
.gitattributes
vendored
@@ -1 +1,131 @@
|
|||||||
*.blend filter=lfs diff=lfs merge=lfs -text
|
# Define macros (only works in top-level gitattributes files)
|
||||||
|
[attr]lfs filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
# 3D models
|
||||||
|
*.collada lfs
|
||||||
|
*.dae lfs
|
||||||
|
*.dxf lfs
|
||||||
|
*.FBX lfs
|
||||||
|
*.fbx lfs
|
||||||
|
*.jas lfs
|
||||||
|
*.lws lfs
|
||||||
|
*.lxo lfs
|
||||||
|
*.obj lfs
|
||||||
|
*.ply lfs
|
||||||
|
*.skp lfs
|
||||||
|
*.stl lfs
|
||||||
|
*.stp lfs
|
||||||
|
*.usd lfs
|
||||||
|
*.usda lfs
|
||||||
|
*.usdc lfs
|
||||||
|
*.usdz lfs
|
||||||
|
*.glb lfs
|
||||||
|
*.gltf lfs
|
||||||
|
|
||||||
|
# Audio
|
||||||
|
*.aif lfs
|
||||||
|
*.aiff lfs
|
||||||
|
*.it lfs
|
||||||
|
*.mod lfs
|
||||||
|
*.mp3 lfs
|
||||||
|
*.ogg lfs
|
||||||
|
*.s3m lfs
|
||||||
|
*.wav lfs
|
||||||
|
*.xm lfs
|
||||||
|
|
||||||
|
# Video
|
||||||
|
*.asf lfs
|
||||||
|
*.avi lfs
|
||||||
|
*.flv lfs
|
||||||
|
*.mov lfs
|
||||||
|
*.mp4 lfs
|
||||||
|
*.mpeg lfs
|
||||||
|
*.mpg lfs
|
||||||
|
*.ogv lfs
|
||||||
|
*.wmv lfs
|
||||||
|
*.mkv lfs
|
||||||
|
|
||||||
|
# Images
|
||||||
|
*.bmp lfs
|
||||||
|
*.exr lfs
|
||||||
|
*.gif lfs
|
||||||
|
*.hdr lfs
|
||||||
|
*.iff lfs
|
||||||
|
*.jpeg lfs
|
||||||
|
*.jpg lfs
|
||||||
|
*.pict lfs
|
||||||
|
*.png lfs
|
||||||
|
*.psd lfs
|
||||||
|
*.tga lfs
|
||||||
|
*.tif lfs
|
||||||
|
*.tiff lfs
|
||||||
|
*.webp lfs
|
||||||
|
|
||||||
|
# Compressed Archive
|
||||||
|
*.7z lfs
|
||||||
|
*.bz2 lfs
|
||||||
|
*.gz lfs
|
||||||
|
*.rar lfs
|
||||||
|
*.tar lfs
|
||||||
|
*.zip lfs
|
||||||
|
|
||||||
|
# Compiled Dynamic Library
|
||||||
|
*.dll lfs
|
||||||
|
*.pdb lfs
|
||||||
|
*.so lfs
|
||||||
|
|
||||||
|
# Fonts
|
||||||
|
*.otf lfs
|
||||||
|
*.ttf lfs
|
||||||
|
*.woff lfs
|
||||||
|
*.woff2 lfs
|
||||||
|
|
||||||
|
# Executable/Installer
|
||||||
|
*.apk lfs
|
||||||
|
*.exe lfs
|
||||||
|
|
||||||
|
# Documents
|
||||||
|
*.pdf lfs
|
||||||
|
*.doc lfs
|
||||||
|
*.xls lfs
|
||||||
|
*.ppt lfs
|
||||||
|
*.docx lfs
|
||||||
|
*.xlsx lfs
|
||||||
|
*.pptx lfs
|
||||||
|
|
||||||
|
# Adobe
|
||||||
|
*.psd lfs
|
||||||
|
*.ai lfs
|
||||||
|
*.aep lfs
|
||||||
|
*.prproj lfs
|
||||||
|
*.spp lfs
|
||||||
|
|
||||||
|
# Zbrush
|
||||||
|
*.zbr lfs
|
||||||
|
*.zpr lfs
|
||||||
|
*.ztl lfs
|
||||||
|
*.c4d lfs
|
||||||
|
|
||||||
|
# Autodesk
|
||||||
|
*.max lfs
|
||||||
|
*.ma lfs
|
||||||
|
*.mb lfs
|
||||||
|
*.3dm lfs
|
||||||
|
*.3ds lfs
|
||||||
|
|
||||||
|
# Blender
|
||||||
|
*.blend lfs
|
||||||
|
*.blend1 lfs
|
||||||
|
*.c4d lfs
|
||||||
|
|
||||||
|
# Imaging
|
||||||
|
*.dicom lfs
|
||||||
|
*.dcm lfs
|
||||||
|
*.nii lfs
|
||||||
|
*.ome lfs
|
||||||
|
*.map lfs
|
||||||
|
*.mrc lfs
|
||||||
|
|
||||||
|
# Others
|
||||||
|
*.abr lfs
|
||||||
|
*.tpl lfs
|
||||||
300
.gitignore
vendored
300
.gitignore
vendored
@@ -1,6 +1,51 @@
|
|||||||
|
# Tempary files
|
||||||
|
temp
|
||||||
|
sandbox.*
|
||||||
|
notes/
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.envrc
|
||||||
|
.venv
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
.secrets
|
||||||
|
.secrets.*
|
||||||
|
!.secrets.example
|
||||||
|
secrets/
|
||||||
|
SECRETS/
|
||||||
|
secrets.bak/
|
||||||
|
|
||||||
|
# Claude
|
||||||
|
.claudian
|
||||||
|
|
||||||
|
# Obsidian
|
||||||
|
|
||||||
|
.obsidian/*
|
||||||
|
!.obsidian/snippets
|
||||||
|
!.obsidian/app.json
|
||||||
|
|
||||||
|
*.canvas
|
||||||
|
|
||||||
|
# Keep odox for extract to openwiki
|
||||||
|
!.obsidian/plugins
|
||||||
|
.obsidian/plugins/*
|
||||||
|
!.obsidian/plugins/odox
|
||||||
|
|
||||||
|
# Blender
|
||||||
|
*.blend1
|
||||||
|
blender_assets.cats.txt~
|
||||||
|
blendcache_*
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[codz]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
|
||||||
# C extensions
|
# C extensions
|
||||||
@@ -20,7 +65,6 @@ parts/
|
|||||||
sdist/
|
sdist/
|
||||||
var/
|
var/
|
||||||
wheels/
|
wheels/
|
||||||
pip-wheel-metadata/
|
|
||||||
share/python-wheels/
|
share/python-wheels/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
.installed.cfg
|
.installed.cfg
|
||||||
@@ -47,8 +91,10 @@ htmlcov/
|
|||||||
nosetests.xml
|
nosetests.xml
|
||||||
coverage.xml
|
coverage.xml
|
||||||
*.cover
|
*.cover
|
||||||
|
*.py.cover
|
||||||
.hypothesis/
|
.hypothesis/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
*.mo
|
*.mo
|
||||||
@@ -71,6 +117,7 @@ instance/
|
|||||||
docs/_build/
|
docs/_build/
|
||||||
|
|
||||||
# PyBuilder
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
target/
|
target/
|
||||||
|
|
||||||
# Jupyter Notebook
|
# Jupyter Notebook
|
||||||
@@ -81,30 +128,70 @@ profile_default/
|
|||||||
ipython_config.py
|
ipython_config.py
|
||||||
|
|
||||||
# pyenv
|
# pyenv
|
||||||
.python-version
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
# pipenv
|
# pipenv
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
# install all needed dependencies.
|
# install all needed dependencies.
|
||||||
#Pipfile.lock
|
# Pipfile.lock
|
||||||
|
|
||||||
# celery beat schedule file
|
# UV
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# uv.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
# poetry.lock
|
||||||
|
# poetry.toml
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
||||||
|
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
||||||
|
# pdm.lock
|
||||||
|
# pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
|
# pixi
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
||||||
|
# pixi.lock
|
||||||
|
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
||||||
|
# in the .venv directory. It is recommended not to include this directory in version control.
|
||||||
|
.pixi
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
celerybeat-schedule
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
*.rdb
|
||||||
|
*.aof
|
||||||
|
*.pid
|
||||||
|
|
||||||
|
# RabbitMQ
|
||||||
|
mnesia/
|
||||||
|
rabbitmq/
|
||||||
|
rabbitmq-data/
|
||||||
|
|
||||||
|
# ActiveMQ
|
||||||
|
activemq-data/
|
||||||
|
|
||||||
# SageMath parsed files
|
# SageMath parsed files
|
||||||
*.sage.py
|
*.sage.py
|
||||||
|
|
||||||
# Environments
|
|
||||||
.env
|
|
||||||
.venv
|
|
||||||
env/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
.spyderproject
|
.spyderproject
|
||||||
.spyproject
|
.spyproject
|
||||||
@@ -123,18 +210,183 @@ dmypy.json
|
|||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
pythonlib*
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
*.ipynb
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
.secrets
|
# PyCharm
|
||||||
.vdb
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
# .idea/
|
||||||
|
|
||||||
!scipy_ndimage/*/**
|
# Abstra
|
||||||
|
# Abstra is an AI-powered process automation framework.
|
||||||
|
# Ignore directories containing user credentials, local state, and settings.
|
||||||
|
# Learn more at https://abstra.io/docs
|
||||||
|
.abstra/
|
||||||
|
|
||||||
# Blender
|
# Visual Studio Code
|
||||||
*.blend1
|
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||||
blendcache_*
|
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||||
*.cats.txt~
|
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||||
|
# you could uncomment the following to ignore the entire vscode folder
|
||||||
|
# .vscode/
|
||||||
|
|
||||||
tests/data
|
# Ruff stuff:
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# PyPI configuration file
|
||||||
|
.pypirc
|
||||||
|
|
||||||
|
# Marimo
|
||||||
|
marimo/_static/
|
||||||
|
marimo/_lsp/
|
||||||
|
__marimo__/
|
||||||
|
|
||||||
|
# Streamlit
|
||||||
|
.streamlit/secrets.toml
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
.output
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Sveltekit cache directory
|
||||||
|
.svelte-kit/
|
||||||
|
|
||||||
|
# vitepress build output
|
||||||
|
**/.vitepress/dist
|
||||||
|
|
||||||
|
# vitepress cache directory
|
||||||
|
**/.vitepress/cache
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# Firebase cache directory
|
||||||
|
.firebase/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
.pnpm-store
|
||||||
|
|
||||||
|
# yarn v3
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# Vite files
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
|
.vite/
|
||||||
22
.obsidian/app.json
vendored
Normal file
22
.obsidian/app.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"showInlineTitle": false,
|
||||||
|
"useMarkdownLinks": true,
|
||||||
|
"pdfExportSettings": {
|
||||||
|
"pageSize": "Letter",
|
||||||
|
"landscape": false,
|
||||||
|
"margin": "0",
|
||||||
|
"downscalePercent": 100
|
||||||
|
},
|
||||||
|
"userIgnoreFilters": [
|
||||||
|
"notes/assets/"
|
||||||
|
],
|
||||||
|
"showUnsupportedFiles": false,
|
||||||
|
"attachmentFolderPath": "notes/assets",
|
||||||
|
"livePreview": true,
|
||||||
|
"showIndentGuide": false,
|
||||||
|
"alwaysUpdateLinks": true,
|
||||||
|
"openBehavior": "file:README",
|
||||||
|
"newFileLocation": "folder",
|
||||||
|
"newFileFolderPath": "notes",
|
||||||
|
"newLinkFormat": "relative"
|
||||||
|
}
|
||||||
30
.obsidian/snippets/omoolab.css
vendored
Normal file
30
.obsidian/snippets/omoolab.css
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/* not show label in canvas */
|
||||||
|
.canvas-node-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-node-container:hover + .canvas-node-label {
|
||||||
|
display: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* image caption */
|
||||||
|
.image-embed {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-embed[alt]:after {
|
||||||
|
content: attr(alt);
|
||||||
|
display: block;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
line-height: 2.0;
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--text-faint);
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* fix image in canvas */
|
||||||
|
.image-embed {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.11
|
||||||
236
AGENTS.md
236
AGENTS.md
@@ -1,227 +1,19 @@
|
|||||||
# BioxelNodes - Agent Development Guide
|
## 总规范
|
||||||
|
|
||||||
This file contains essential information for agentic coding agents working on the BioxelNodes codebase.
|
- 说明性内容(例如提交说明、文档、注释、mermaid),以中文为主,专有名词、约定俗称可用英文
|
||||||
|
|
||||||
## Project Overview
|
## Markdown 规范
|
||||||
|
|
||||||
BioxelNodes is a Blender addon for scientific volumetric data visualization. It integrates with Blender's Geometry Nodes and Cycles rendering engine to process and visualize medical/scientific data.
|
- 不使用`---`分割器
|
||||||
|
- `# Heading` 一级标题仅用于开头
|
||||||
|
- mermaid 节点名用英文字符
|
||||||
|
|
||||||
**Language:** Python 3.11
|
## 代码规范
|
||||||
**Framework:** Blender Python API (bpy)
|
|
||||||
**Package Manager:** uv
|
|
||||||
|
|
||||||
## Development Commands
|
- 避免嵌套结构
|
||||||
|
- 特殊的、非常规的需要写注释
|
||||||
### Environment Setup
|
- 变量、函数名、Docstring 用全英文
|
||||||
```bash
|
- 同一事物用统一表达
|
||||||
# Install dependencies
|
- 使用的依赖越少越好
|
||||||
uv sync
|
- 选用简单直接的方式实现
|
||||||
```
|
- 代码、执行方式改变,调整已有的文档、计划,而非新建
|
||||||
|
|
||||||
### Build Commands
|
|
||||||
```bash
|
|
||||||
# Build for specific platform
|
|
||||||
uv run build.py <platform> # platforms: windows-x64, linux-x64, macos-arm64, macos-x64
|
|
||||||
```
|
|
||||||
|
|
||||||
### Unit Test
|
|
||||||
```bash
|
|
||||||
uv run pytest tests/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Code Quality
|
|
||||||
```bash
|
|
||||||
# Format code (autopep8)
|
|
||||||
autopep8 --in-place --recursive src/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
```bash
|
|
||||||
# Serve documentation locally
|
|
||||||
uv run mkdocs serve
|
|
||||||
|
|
||||||
# Build documentation
|
|
||||||
uv run mike deploy --push --update-aliases 0.2.x latest
|
|
||||||
```
|
|
||||||
|
|
||||||
## Code Style Guidelines
|
|
||||||
|
|
||||||
### Formatting
|
|
||||||
- **4-space indentation**
|
|
||||||
- **PEP 8** compliance enforced via autopep8
|
|
||||||
|
|
||||||
### Naming Conventions
|
|
||||||
- **snake_case** for functions, variables, and files
|
|
||||||
- **PascalCase** for classes (Blender convention)
|
|
||||||
- **UPPER_CASE** for constants
|
|
||||||
- **bioxel_** prefix for addon-specific properties
|
|
||||||
|
|
||||||
### Import Organization
|
|
||||||
```python
|
|
||||||
# Standard library imports first
|
|
||||||
import pathlib
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
# Third-party imports
|
|
||||||
import bpy
|
|
||||||
import numpy as np
|
|
||||||
import SimpleITK as sitk
|
|
||||||
|
|
||||||
# Relative imports for internal modules
|
|
||||||
from ..exceptions import CancelledByUser
|
|
||||||
from ..utils import get_layer_obj
|
|
||||||
```
|
|
||||||
|
|
||||||
### Type Hints
|
|
||||||
- Use sparingly, mainly for Blender property annotations
|
|
||||||
- Focus on function signatures and complex data structures
|
|
||||||
- Example: `def process_data(data: np.ndarray) -> np.ndarray:`
|
|
||||||
|
|
||||||
## Architecture Patterns
|
|
||||||
|
|
||||||
### Auto-Registration System
|
|
||||||
- Uses `auto_load.py` for automatic class registration
|
|
||||||
- Classes are discovered automatically via reflection
|
|
||||||
- Registration order resolved via topological sorting
|
|
||||||
- All Blender classes must inherit from appropriate base types
|
|
||||||
|
|
||||||
### Module Structure
|
|
||||||
```
|
|
||||||
src/bioxelnodes/
|
|
||||||
├── __init__.py # Addon entry point, asset library setup
|
|
||||||
├── auto_load.py # Auto-registration system
|
|
||||||
├── constants.py # Constants and paths
|
|
||||||
├── utils.py # Utility functions
|
|
||||||
├── preferences.py # Addon preferences
|
|
||||||
├── props.py # Blender properties
|
|
||||||
├── panels.py # UI panels
|
|
||||||
├── menus.py # UI menus
|
|
||||||
├── node.py # Node utilities
|
|
||||||
├── layer.py # Layer management
|
|
||||||
├── operators/ # Blender operators
|
|
||||||
├── bioxel/ # Core bioxel functionality
|
|
||||||
└── assets/ # Blender assets and node libraries
|
|
||||||
```
|
|
||||||
|
|
||||||
### Blender Integration Patterns
|
|
||||||
|
|
||||||
#### Property Groups
|
|
||||||
```python
|
|
||||||
class BioxelProperties(bpy.types.PropertyGroup):
|
|
||||||
bl_label = "Bioxel Properties"
|
|
||||||
|
|
||||||
bioxel_custom_prop: bpy.props.StringProperty(
|
|
||||||
name="Custom Property",
|
|
||||||
default="",
|
|
||||||
description="Description for UI"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Operators
|
|
||||||
```python
|
|
||||||
class BIOXEL_OT_custom_operator(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxel.custom_operator"
|
|
||||||
bl_label = "Custom Operator"
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
# Operator logic here
|
|
||||||
return {'FINISHED'}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Panels
|
|
||||||
```python
|
|
||||||
class BIOXEL_PT_custom_panel(bpy.types.Panel):
|
|
||||||
bl_label = "Custom Panel"
|
|
||||||
bl_idname = "BIOXEL_PT_custom_panel"
|
|
||||||
bl_space_type = 'VIEW_3D'
|
|
||||||
bl_region_type = 'UI'
|
|
||||||
bl_category = "Bioxel"
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
# UI drawing code here
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
### Error Handling Patterns
|
|
||||||
- Use try-catch blocks for file operations
|
|
||||||
- Handle user cancellation gracefully
|
|
||||||
- Report progress for long operations using `bioxel_progress_factor` and `bioxel_progress_text`
|
|
||||||
- Log warnings with print statements for debugging
|
|
||||||
|
|
||||||
|
|
||||||
## Key Dependencies
|
|
||||||
|
|
||||||
### Core Libraries
|
|
||||||
- **bpy** (Blender Python API) - Core integration
|
|
||||||
- **numpy** - Array processing
|
|
||||||
- **SimpleITK** - Medical image processing
|
|
||||||
- **h5py** - HDF5 file handling
|
|
||||||
|
|
||||||
### Scientific Libraries
|
|
||||||
- **matplotlib** - Plotting and visualization
|
|
||||||
- **transforms3d** - 3D transformations
|
|
||||||
- **pyometiff** - OME-TIFF support
|
|
||||||
- **mrcfile** - MRC file format support
|
|
||||||
|
|
||||||
## Development Workflow
|
|
||||||
|
|
||||||
### Making Changes
|
|
||||||
1. Identify the appropriate module (operators, panels, bioxel, etc.)
|
|
||||||
2. Follow existing naming conventions and patterns
|
|
||||||
3. Use auto-registration - no manual registration needed
|
|
||||||
4. Test with Blender's Python console
|
|
||||||
5. Format code with autopep8 before committing
|
|
||||||
|
|
||||||
### Adding New Features
|
|
||||||
1. Create appropriate classes in relevant modules
|
|
||||||
2. Use proper prefixes (BIOXEL_OT_, BIOXEL_PT_, etc.)
|
|
||||||
3. Add to auto_load system automatically discovers new classes
|
|
||||||
4. Update documentation if needed
|
|
||||||
|
|
||||||
### File Operations
|
|
||||||
- Use `pathlib.Path` for path operations
|
|
||||||
- Check file existence before operations
|
|
||||||
- Handle permissions and missing files gracefully
|
|
||||||
- Use appropriate file formats (HDF5 for data, PNG for images)
|
|
||||||
|
|
||||||
## Blender Integration Notes
|
|
||||||
|
|
||||||
### Asset Library
|
|
||||||
- Automatically managed via `add_asset_library_if_missing()`
|
|
||||||
- Located at `NODE_LIB_DIRPATH`
|
|
||||||
- Uses "PACK" import method
|
|
||||||
- Name: "O Bioxel"
|
|
||||||
|
|
||||||
### Progress Reporting
|
|
||||||
- Use `bioxel_progress_factor` (0.0 to 1.0) for progress
|
|
||||||
- Use `bioxel_progress_text` for status messages
|
|
||||||
- Update via WindowManager properties
|
|
||||||
|
|
||||||
### Layer Management
|
|
||||||
- Layers stored as HDF5 files
|
|
||||||
- Container objects manage multiple layers
|
|
||||||
- Use `layer.py` for layer operations
|
|
||||||
- Support for various medical image formats
|
|
||||||
|
|
||||||
## Common Pitfalls
|
|
||||||
|
|
||||||
### Blender API
|
|
||||||
- Always check context validity
|
|
||||||
- Handle different Blender versions (check `bpy.app.version`)
|
|
||||||
- Use proper property types for UI
|
|
||||||
- Register classes in correct order (auto_load handles this)
|
|
||||||
|
|
||||||
### Performance
|
|
||||||
- Use NumPy for array operations
|
|
||||||
- Avoid expensive operations in UI draw calls
|
|
||||||
- Cache computed values where appropriate
|
|
||||||
- Use background threads for long operations
|
|
||||||
|
|
||||||
### File I/O
|
|
||||||
- Always close file handles
|
|
||||||
- Use context managers for file operations
|
|
||||||
- Handle large files in chunks
|
|
||||||
- Validate file formats before processing
|
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
# BioxelNodes Unit Tests
|
|
||||||
|
|
||||||
This directory contains unit tests for the bioxel module.
|
|
||||||
|
|
||||||
## Test Data
|
|
||||||
|
|
||||||
Test files should be placed in `tests/data/`:
|
|
||||||
|
|
||||||
## Running Tests
|
|
||||||
|
|
||||||
Run all tests:
|
|
||||||
```bash
|
|
||||||
uv run pytest
|
|
||||||
```
|
|
||||||
|
|
||||||
Run specific test file:
|
|
||||||
```bash
|
|
||||||
uv run pytest tests/test_bioxel.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Run specific test class:
|
|
||||||
```bash
|
|
||||||
uv run pytest tests/test_bioxel.py::TestDataClass
|
|
||||||
```
|
|
||||||
|
|
||||||
Run with verbose output:
|
|
||||||
```bash
|
|
||||||
uv run pytest -v
|
|
||||||
```
|
|
||||||
|
|
||||||
Run with coverage:
|
|
||||||
```bash
|
|
||||||
uv run pytest --cov=bioxelnodes.bioxel --cov-report=html
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test Categories
|
|
||||||
|
|
||||||
- **TestDataClass**: Data class initialization and properties
|
|
||||||
- **TestDataToLayers**: Data.to_layers() method
|
|
||||||
- **TestUtilityFunctions**: Utility functions
|
|
||||||
- **TestLayerClass**: Layer class functionality
|
|
||||||
- **TestConstants**: Module constants
|
|
||||||
- **TestEdgeCases**: Edge cases and error handling
|
|
||||||
@@ -1,539 +0,0 @@
|
|||||||
"""
|
|
||||||
Unit tests for bioxel module.
|
|
||||||
|
|
||||||
Tests cover:
|
|
||||||
- Data class functionality
|
|
||||||
- Parsing NRRD and DICOM formats
|
|
||||||
- Layer creation and transformation
|
|
||||||
- Utility functions
|
|
||||||
"""
|
|
||||||
|
|
||||||
from bioxelnodes.bioxel import (
|
|
||||||
Data,
|
|
||||||
read,
|
|
||||||
read_meta,
|
|
||||||
calc_layer_shape,
|
|
||||||
calc_layer_size,
|
|
||||||
Layer,
|
|
||||||
)
|
|
||||||
import pytest
|
|
||||||
import numpy as np
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
||||||
|
|
||||||
|
|
||||||
# Test data files
|
|
||||||
NRRD_FILE = Path(__file__).parent / "data/VHP_M.nrrd"
|
|
||||||
DICOM_FILE = Path(__file__).parent / "data/VHP_M_CT_Head/IM000001.dcm"
|
|
||||||
DICOM_DIR = Path(__file__).parent / "data/VHP_M_CT_Head/"
|
|
||||||
|
|
||||||
|
|
||||||
class TestDataClass:
|
|
||||||
"""Test Data class initialization and properties"""
|
|
||||||
|
|
||||||
def test_data_initialization(self):
|
|
||||||
"""Test basic Data initialization with NRRD"""
|
|
||||||
data = Data(filepath=str(NRRD_FILE), series_id="")
|
|
||||||
|
|
||||||
assert data.filepath is not None
|
|
||||||
assert data.series_id == ""
|
|
||||||
assert data._data is None
|
|
||||||
assert data._meta is None
|
|
||||||
|
|
||||||
def test_read_meta_nrrd(self):
|
|
||||||
"""Test reading only meta from NRRD file"""
|
|
||||||
data_obj = read_meta(str(NRRD_FILE))
|
|
||||||
|
|
||||||
assert data_obj._data is None
|
|
||||||
assert data_obj._meta is not None
|
|
||||||
assert "name" in data_obj.meta
|
|
||||||
assert "spacing" in data_obj.meta
|
|
||||||
assert "affine" in data_obj.meta
|
|
||||||
assert "xyz_shape" in data_obj.meta
|
|
||||||
assert "frame_count" in data_obj.meta
|
|
||||||
assert "channel_count" in data_obj.meta
|
|
||||||
assert "dtype" in data_obj.meta
|
|
||||||
|
|
||||||
def test_read_meta_dicom(self):
|
|
||||||
"""Test reading only meta from DICOM file"""
|
|
||||||
data_obj = read_meta(str(DICOM_FILE), series_id="")
|
|
||||||
|
|
||||||
assert data_obj._data is None
|
|
||||||
assert data_obj._meta is not None
|
|
||||||
assert "name" in data_obj.meta
|
|
||||||
|
|
||||||
def test_read_full_data_nrrd(self):
|
|
||||||
"""Test reading full NRRD data"""
|
|
||||||
data_obj = read(str(NRRD_FILE))
|
|
||||||
|
|
||||||
assert data_obj._data is not None
|
|
||||||
assert data_obj._meta is not None
|
|
||||||
assert isinstance(data_obj.data, np.ndarray)
|
|
||||||
assert data_obj.data.ndim == 5
|
|
||||||
|
|
||||||
def test_read_full_data_dicom(self):
|
|
||||||
"""Test reading full DICOM data"""
|
|
||||||
data_obj = read(str(DICOM_FILE), series_id="")
|
|
||||||
|
|
||||||
assert data_obj._data is not None
|
|
||||||
assert data_obj._meta is not None
|
|
||||||
assert isinstance(data_obj.data, np.ndarray)
|
|
||||||
|
|
||||||
def test_data_properties_nrrd(self):
|
|
||||||
"""Test Data property accessors"""
|
|
||||||
data_obj = read(str(NRRD_FILE))
|
|
||||||
|
|
||||||
assert hasattr(data_obj, "name")
|
|
||||||
assert hasattr(data_obj, "description")
|
|
||||||
assert hasattr(data_obj, "shape")
|
|
||||||
assert hasattr(data_obj, "xyz_shape")
|
|
||||||
assert hasattr(data_obj, "spacing")
|
|
||||||
assert hasattr(data_obj, "affine")
|
|
||||||
assert hasattr(data_obj, "frame_count")
|
|
||||||
assert hasattr(data_obj, "channel_count")
|
|
||||||
assert hasattr(data_obj, "dtype")
|
|
||||||
|
|
||||||
assert isinstance(data_obj.shape, tuple)
|
|
||||||
assert isinstance(data_obj.xyz_shape, tuple)
|
|
||||||
assert isinstance(data_obj.spacing, tuple)
|
|
||||||
assert isinstance(data_obj.affine, np.ndarray)
|
|
||||||
assert isinstance(data_obj.dtype, np.dtype)
|
|
||||||
|
|
||||||
def test_data_lazy_loading(self):
|
|
||||||
"""Test lazy loading behavior"""
|
|
||||||
data_obj = Data(filepath=str(NRRD_FILE), series_id="")
|
|
||||||
|
|
||||||
assert data_obj.is_loaded() is False
|
|
||||||
|
|
||||||
data_obj.load_meta()
|
|
||||||
assert data_obj._data is None
|
|
||||||
assert data_obj._meta is not None
|
|
||||||
|
|
||||||
data_obj.load_data()
|
|
||||||
assert data_obj._data is not None
|
|
||||||
assert data_obj.is_loaded() is True
|
|
||||||
|
|
||||||
def test_shape_property(self):
|
|
||||||
"""Test shape property returns TXYZC format"""
|
|
||||||
data_obj = read(str(NRRD_FILE))
|
|
||||||
|
|
||||||
shape = data_obj.shape
|
|
||||||
assert len(shape) == 5
|
|
||||||
assert shape[0] == data_obj.meta["frame_count"]
|
|
||||||
assert shape[1:4] == data_obj.meta["xyz_shape"]
|
|
||||||
assert shape[4] == data_obj.meta["channel_count"]
|
|
||||||
|
|
||||||
def test_load_meta_method(self):
|
|
||||||
"""Test load_meta() method exists and works"""
|
|
||||||
data_obj = Data(filepath=str(NRRD_FILE), series_id="")
|
|
||||||
|
|
||||||
assert hasattr(data_obj, "load_meta")
|
|
||||||
|
|
||||||
data_obj.load_meta()
|
|
||||||
assert data_obj._meta is not None
|
|
||||||
assert data_obj._data is None
|
|
||||||
|
|
||||||
|
|
||||||
class TestDataToLayers:
|
|
||||||
"""Test Data.to_layers() method"""
|
|
||||||
|
|
||||||
def test_to_layers_dicom_scalar(self):
|
|
||||||
"""Test converting DICOM data to scalar layers"""
|
|
||||||
data_obj = read(str(DICOM_FILE), series_id="")
|
|
||||||
|
|
||||||
layers = data_obj.to_layers(kind="scalar", bioxel_size=2.0)
|
|
||||||
|
|
||||||
assert len(layers) == 1
|
|
||||||
assert layers[0].kind == "scalar"
|
|
||||||
assert isinstance(layers[0], Layer)
|
|
||||||
|
|
||||||
def test_to_layers_nrrd_scalar(self):
|
|
||||||
"""Test converting NRRD data to scalar layers"""
|
|
||||||
data_obj = read(str(NRRD_FILE))
|
|
||||||
|
|
||||||
layers = data_obj.to_layers(kind="scalar", bioxel_size=2.0)
|
|
||||||
|
|
||||||
assert len(layers) == 1
|
|
||||||
assert layers[0].kind == "scalar"
|
|
||||||
assert isinstance(layers[0], Layer)
|
|
||||||
|
|
||||||
def test_to_layers_with_remap(self):
|
|
||||||
"""Test data remapping"""
|
|
||||||
data_obj = read(str(NRRD_FILE))
|
|
||||||
|
|
||||||
layers = data_obj.to_layers(kind="scalar", bioxel_size=2.0, remap=True)
|
|
||||||
|
|
||||||
assert len(layers) == 1
|
|
||||||
assert layers[0].data.dtype == np.float32
|
|
||||||
assert np.all(layers[0].data >= 0)
|
|
||||||
assert np.all(layers[0].data <= 1)
|
|
||||||
|
|
||||||
def test_to_layers_with_bioxel_size(self):
|
|
||||||
"""Test with different bioxel sizes"""
|
|
||||||
data_obj = read(str(NRRD_FILE))
|
|
||||||
|
|
||||||
layers_1x = data_obj.to_layers(kind="scalar", bioxel_size=1.0)
|
|
||||||
layers_2x = data_obj.to_layers(kind="scalar", bioxel_size=2.0)
|
|
||||||
|
|
||||||
assert len(layers_1x) == 1
|
|
||||||
assert len(layers_2x) == 1
|
|
||||||
|
|
||||||
# Larger bioxel size means smaller layer
|
|
||||||
shape_1x = layers_1x[0].shape
|
|
||||||
shape_2x = layers_2x[0].shape
|
|
||||||
assert shape_2x[0] <= shape_1x[0]
|
|
||||||
assert shape_2x[1] <= shape_1x[1]
|
|
||||||
assert shape_2x[2] <= shape_1x[2]
|
|
||||||
|
|
||||||
def test_to_layers_with_smooth(self):
|
|
||||||
"""Test with smoothing parameter"""
|
|
||||||
data_obj = read(str(NRRD_FILE))
|
|
||||||
|
|
||||||
layers = data_obj.to_layers(kind="scalar", bioxel_size=2.0, smooth=1)
|
|
||||||
|
|
||||||
assert len(layers) == 1
|
|
||||||
assert isinstance(layers[0], Layer)
|
|
||||||
|
|
||||||
def test_to_layers_frame_source_first_frame(self):
|
|
||||||
"""Test frame_source = '-1' (first frame only)"""
|
|
||||||
data_obj = read(str(NRRD_FILE))
|
|
||||||
|
|
||||||
layers = data_obj.to_layers(kind="scalar", bioxel_size=2.0, frame_source="-1")
|
|
||||||
|
|
||||||
assert len(layers) == 1
|
|
||||||
assert layers[0].frame_count == 1
|
|
||||||
|
|
||||||
|
|
||||||
class TestUtilityFunctions:
|
|
||||||
"""Test utility functions"""
|
|
||||||
|
|
||||||
def test_calc_layer_shape(self):
|
|
||||||
"""Test layer shape calculation"""
|
|
||||||
shape = calc_layer_shape(
|
|
||||||
bioxel_size=1.0, orig_shape=(100, 100, 100), orig_spacing=(1.0, 1.0, 1.0)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert len(shape) == 3
|
|
||||||
assert shape[0] == 100
|
|
||||||
assert shape[1] == 100
|
|
||||||
assert shape[2] == 100
|
|
||||||
|
|
||||||
def test_calc_layer_shape_small_bioxel(self):
|
|
||||||
"""Test layer shape with small bioxel size"""
|
|
||||||
shape = calc_layer_shape(
|
|
||||||
bioxel_size=2.0, orig_shape=(100, 100, 100), orig_spacing=(1.0, 1.0, 1.0)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert len(shape) == 3
|
|
||||||
assert shape[0] == 50
|
|
||||||
assert shape[1] == 50
|
|
||||||
assert shape[2] == 50
|
|
||||||
|
|
||||||
def test_calc_layer_shape_minimum(self):
|
|
||||||
"""Test layer shape doesn't go below 1"""
|
|
||||||
shape = calc_layer_shape(
|
|
||||||
bioxel_size=100.0, orig_shape=(100, 100, 100), orig_spacing=(1.0, 1.0, 1.0)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert shape == (1, 1, 1)
|
|
||||||
|
|
||||||
def test_calc_layer_size(self):
|
|
||||||
"""Test layer size calculation"""
|
|
||||||
size = calc_layer_size(shape=(100, 100, 100), bioxel_size=1.0, scale=1.0)
|
|
||||||
|
|
||||||
assert len(size) == 3
|
|
||||||
assert size[0] == 100.0
|
|
||||||
assert size[1] == 100.0
|
|
||||||
assert size[2] == 100.0
|
|
||||||
|
|
||||||
def test_calc_layer_size_with_scale(self):
|
|
||||||
"""Test layer size with scale factor"""
|
|
||||||
size = calc_layer_size(shape=(100, 100, 100), bioxel_size=1.0, scale=0.01)
|
|
||||||
|
|
||||||
assert len(size) == 3
|
|
||||||
assert size[0] == 1.0
|
|
||||||
assert size[1] == 1.0
|
|
||||||
assert size[2] == 1.0
|
|
||||||
|
|
||||||
|
|
||||||
class TestLayerClass:
|
|
||||||
"""Test Layer class functionality"""
|
|
||||||
|
|
||||||
def test_layer_initialization(self):
|
|
||||||
"""Test Layer initialization"""
|
|
||||||
data = np.random.rand(1, 8, 8, 8, 1).astype(np.float32)
|
|
||||||
layer = Layer(data=data, name="test_layer", kind="scalar")
|
|
||||||
|
|
||||||
assert layer.name == "test_layer"
|
|
||||||
assert layer.kind == "scalar"
|
|
||||||
assert np.array_equal(layer.data, data)
|
|
||||||
|
|
||||||
def test_layer_properties(self):
|
|
||||||
"""Test Layer property accessors"""
|
|
||||||
data = np.random.rand(1, 8, 8, 8, 1).astype(np.float32)
|
|
||||||
layer = Layer(
|
|
||||||
data=data, name="test_layer", kind="scalar", affine=np.identity(4)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert hasattr(layer, "bioxel_size")
|
|
||||||
assert hasattr(layer, "shape")
|
|
||||||
assert hasattr(layer, "dtype")
|
|
||||||
assert hasattr(layer, "origin")
|
|
||||||
assert hasattr(layer, "euler")
|
|
||||||
assert hasattr(layer, "frame_count")
|
|
||||||
assert hasattr(layer, "channel_count")
|
|
||||||
assert hasattr(layer, "min")
|
|
||||||
assert hasattr(layer, "max")
|
|
||||||
|
|
||||||
def test_layer_shape_property(self):
|
|
||||||
"""Test layer shape returns XYZ (not TXYZC)"""
|
|
||||||
data = np.random.rand(1, 8, 8, 8, 1).astype(np.float32)
|
|
||||||
layer = Layer(data=data, name="test", kind="scalar")
|
|
||||||
|
|
||||||
shape = layer.shape
|
|
||||||
assert len(shape) == 3
|
|
||||||
assert shape[0] == 8
|
|
||||||
assert shape[1] == 8
|
|
||||||
assert shape[2] == 8
|
|
||||||
|
|
||||||
def test_layer_affine_property(self):
|
|
||||||
"""Test affine transformation matrix"""
|
|
||||||
data = np.random.rand(1, 8, 8, 8, 1).astype(np.float32)
|
|
||||||
affine = np.array(
|
|
||||||
[
|
|
||||||
[1.0, 0.0, 0.0, 0.0],
|
|
||||||
[0.0, 1.0, 0.0, 0.0],
|
|
||||||
[0.0, 0.0, 1.0, 0.0],
|
|
||||||
[0.0, 0.0, 0.0, 1.0],
|
|
||||||
]
|
|
||||||
)
|
|
||||||
layer = Layer(data=data, name="test", kind="scalar", affine=affine)
|
|
||||||
|
|
||||||
result_affine = layer.affine
|
|
||||||
assert result_affine.shape == (4, 4)
|
|
||||||
assert np.allclose(result_affine, affine)
|
|
||||||
|
|
||||||
def test_layer_min_max(self):
|
|
||||||
"""Test layer min/max properties"""
|
|
||||||
data = np.random.rand(1, 8, 8, 8, 1).astype(np.float32)
|
|
||||||
layer = Layer(data=data, name="test", kind="scalar")
|
|
||||||
|
|
||||||
assert isinstance(layer.min, float)
|
|
||||||
assert isinstance(layer.max, float)
|
|
||||||
assert layer.min <= layer.max
|
|
||||||
|
|
||||||
def test_layer_copy(self):
|
|
||||||
"""Test layer deep copy"""
|
|
||||||
data = np.random.rand(1, 8, 8, 8, 1).astype(np.float32)
|
|
||||||
layer = Layer(data=data, name="test", kind="scalar")
|
|
||||||
layer_copy = layer.copy()
|
|
||||||
|
|
||||||
assert layer_copy.name == layer.name
|
|
||||||
assert layer_copy.kind == layer.kind
|
|
||||||
assert np.array_equal(layer_copy.data, layer.data)
|
|
||||||
|
|
||||||
assert layer_copy is not layer
|
|
||||||
layer_copy.data[0, 0, 0, 0, 0] = 999.0
|
|
||||||
assert layer.data[0, 0, 0, 0, 0] != 999.0
|
|
||||||
|
|
||||||
def test_layer_invalid_data_shape(self):
|
|
||||||
"""Test Layer raises error for invalid data shape"""
|
|
||||||
data = np.random.rand(8, 8, 8).astype(np.float32)
|
|
||||||
|
|
||||||
with pytest.raises(Exception):
|
|
||||||
Layer(data=data, name="test", kind="scalar")
|
|
||||||
|
|
||||||
def test_layer_invalid_affine_shape(self):
|
|
||||||
"""Test Layer raises error for invalid affine shape"""
|
|
||||||
data = np.random.rand(1, 8, 8, 8, 1).astype(np.float32)
|
|
||||||
affine = np.identity(3)
|
|
||||||
|
|
||||||
with pytest.raises(Exception):
|
|
||||||
Layer(data=data, name="test", kind="scalar", affine=affine)
|
|
||||||
|
|
||||||
|
|
||||||
class TestConstants:
|
|
||||||
"""Test module constants"""
|
|
||||||
|
|
||||||
def test_support_exts_exists(self):
|
|
||||||
"""Test SUPPORT_EXTS constant"""
|
|
||||||
from bioxelnodes.bioxel import SUPPORT_EXTS
|
|
||||||
|
|
||||||
assert isinstance(SUPPORT_EXTS, list)
|
|
||||||
assert len(SUPPORT_EXTS) > 0
|
|
||||||
assert ".h5" in SUPPORT_EXTS
|
|
||||||
assert ".nrrd" in SUPPORT_EXTS
|
|
||||||
assert ".nii.gz" in SUPPORT_EXTS
|
|
||||||
assert ".dcm" in SUPPORT_EXTS
|
|
||||||
|
|
||||||
def test_dicom_exts_exists(self):
|
|
||||||
"""Test DICOM_EXTS constant"""
|
|
||||||
from bioxelnodes.bioxel import DICOM_EXTS
|
|
||||||
|
|
||||||
assert isinstance(DICOM_EXTS, list)
|
|
||||||
assert "" in DICOM_EXTS
|
|
||||||
assert ".dcm" in DICOM_EXTS
|
|
||||||
assert ".DCM" in DICOM_EXTS
|
|
||||||
|
|
||||||
def test_ome_exts_exists(self):
|
|
||||||
"""Test OME_EXTS constant"""
|
|
||||||
from bioxelnodes.bioxel import OME_EXTS
|
|
||||||
|
|
||||||
assert isinstance(OME_EXTS, list)
|
|
||||||
assert ".tiff" in OME_EXTS
|
|
||||||
assert ".ome.tiff" in OME_EXTS
|
|
||||||
|
|
||||||
def test_mrc_exts_exists(self):
|
|
||||||
"""Test MRC_EXTS constant"""
|
|
||||||
from bioxelnodes.bioxel import MRC_EXTS
|
|
||||||
|
|
||||||
assert isinstance(MRC_EXTS, list)
|
|
||||||
assert ".mrc" in MRC_EXTS
|
|
||||||
|
|
||||||
|
|
||||||
class TestEdgeCases:
|
|
||||||
"""Test edge cases and error handling"""
|
|
||||||
|
|
||||||
def test_nonexistent_file(self):
|
|
||||||
"""Test handling of nonexistent file"""
|
|
||||||
with pytest.raises(FileNotFoundError):
|
|
||||||
data_obj = read("/nonexistent/file.nii.gz", "")
|
|
||||||
|
|
||||||
def test_frame_count_property_multiframe(self):
|
|
||||||
"""Test frame_count property for multi-frame data"""
|
|
||||||
data_obj = read(str(NRRD_FILE))
|
|
||||||
|
|
||||||
assert data_obj.frame_count >= 1
|
|
||||||
assert data_obj.frame_count == data_obj.meta["frame_count"]
|
|
||||||
|
|
||||||
def test_channel_count_property(self):
|
|
||||||
"""Test channel_count property"""
|
|
||||||
data_obj = read(str(NRRD_FILE))
|
|
||||||
|
|
||||||
assert data_obj.channel_count >= 1
|
|
||||||
assert data_obj.channel_count == data_obj.meta["channel_count"]
|
|
||||||
|
|
||||||
def test_nrrd_format_support(self):
|
|
||||||
"""Test NRRD format is in supported extensions"""
|
|
||||||
from bioxelnodes.bioxel import SUPPORT_EXTS
|
|
||||||
|
|
||||||
assert ".nrrd" in SUPPORT_EXTS
|
|
||||||
|
|
||||||
def test_dicom_format_support(self):
|
|
||||||
"""Test DICOM format is in supported extensions"""
|
|
||||||
from bioxelnodes.bioxel import SUPPORT_EXTS
|
|
||||||
|
|
||||||
assert ".dcm" in SUPPORT_EXTS
|
|
||||||
assert "" in SUPPORT_EXTS
|
|
||||||
|
|
||||||
|
|
||||||
class TestRealDataFiles:
|
|
||||||
"""Test with real data files"""
|
|
||||||
|
|
||||||
def test_nrrd_file_exists(self):
|
|
||||||
"""Test NRRD test file exists"""
|
|
||||||
assert NRRD_FILE.exists()
|
|
||||||
assert NRRD_FILE.is_file()
|
|
||||||
|
|
||||||
def test_dicom_file_exists(self):
|
|
||||||
"""Test DICOM test file exists"""
|
|
||||||
assert DICOM_FILE.exists()
|
|
||||||
assert DICOM_FILE.is_file()
|
|
||||||
|
|
||||||
def test_dicom_directory_exists(self):
|
|
||||||
"""Test DICOM directory exists"""
|
|
||||||
assert DICOM_DIR.exists()
|
|
||||||
assert DICOM_DIR.is_dir()
|
|
||||||
|
|
||||||
def test_read_nrrd_data(self):
|
|
||||||
"""Test reading real NRRD data"""
|
|
||||||
data_obj = read(str(NRRD_FILE))
|
|
||||||
|
|
||||||
assert data_obj._data is not None
|
|
||||||
assert data_obj._meta is not None
|
|
||||||
assert isinstance(data_obj.data, np.ndarray)
|
|
||||||
assert data_obj.data.size > 0
|
|
||||||
|
|
||||||
def test_read_dicom_data(self):
|
|
||||||
"""Test reading real DICOM data"""
|
|
||||||
data_obj = read(str(DICOM_FILE), series_id="")
|
|
||||||
|
|
||||||
assert data_obj._data is not None
|
|
||||||
assert data_obj._meta is not None
|
|
||||||
assert isinstance(data_obj.data, np.ndarray)
|
|
||||||
assert data_obj.data.size > 0
|
|
||||||
|
|
||||||
def test_nrrd_meta_contains_required_fields(self):
|
|
||||||
"""Test NRRD meta contains all required fields"""
|
|
||||||
data_obj = read(str(NRRD_FILE))
|
|
||||||
meta = data_obj.meta
|
|
||||||
|
|
||||||
required_fields = [
|
|
||||||
"name",
|
|
||||||
"description",
|
|
||||||
"spacing",
|
|
||||||
"affine",
|
|
||||||
"xyz_shape",
|
|
||||||
"frame_count",
|
|
||||||
"channel_count",
|
|
||||||
"dtype",
|
|
||||||
]
|
|
||||||
|
|
||||||
for field in required_fields:
|
|
||||||
assert field in meta, f"Meta missing field: {field}"
|
|
||||||
|
|
||||||
def test_dicom_meta_contains_required_fields(self):
|
|
||||||
"""Test DICOM meta contains all required fields"""
|
|
||||||
data_obj = read(str(DICOM_FILE), series_id="")
|
|
||||||
meta = data_obj.meta
|
|
||||||
|
|
||||||
required_fields = [
|
|
||||||
"name",
|
|
||||||
"description",
|
|
||||||
"spacing",
|
|
||||||
"affine",
|
|
||||||
"xyz_shape",
|
|
||||||
"frame_count",
|
|
||||||
"channel_count",
|
|
||||||
"dtype",
|
|
||||||
]
|
|
||||||
|
|
||||||
for field in required_fields:
|
|
||||||
assert field in meta, f"Meta missing field: {field}"
|
|
||||||
|
|
||||||
def test_data_to_layers_with_real_nrrd(self):
|
|
||||||
"""Test to_layers with real NRRD data"""
|
|
||||||
data_obj = read(str(NRRD_FILE))
|
|
||||||
|
|
||||||
layers = data_obj.to_layers(kind="scalar", bioxel_size=3.0)
|
|
||||||
|
|
||||||
assert len(layers) > 0
|
|
||||||
assert all(isinstance(layer, Layer) for layer in layers)
|
|
||||||
|
|
||||||
def test_data_to_layers_with_real_dicom(self):
|
|
||||||
"""Test to_layers with real DICOM data"""
|
|
||||||
data_obj = read(str(DICOM_FILE), series_id="")
|
|
||||||
|
|
||||||
layers = data_obj.to_layers(kind="scalar", bioxel_size=3.0)
|
|
||||||
|
|
||||||
assert len(layers) > 0
|
|
||||||
assert all(isinstance(layer, Layer) for layer in layers)
|
|
||||||
|
|
||||||
def test_nrrd_spacing_values(self):
|
|
||||||
"""Test NRRD spacing values are reasonable"""
|
|
||||||
data_obj = read(str(NRRD_FILE))
|
|
||||||
spacing = data_obj.spacing
|
|
||||||
|
|
||||||
assert len(spacing) == 3
|
|
||||||
assert all(s > 0 for s in spacing)
|
|
||||||
|
|
||||||
def test_dicom_spacing_values(self):
|
|
||||||
"""Test DICOM spacing values are reasonable"""
|
|
||||||
data_obj = read(str(DICOM_FILE), series_id="")
|
|
||||||
spacing = data_obj.spacing
|
|
||||||
|
|
||||||
assert len(spacing) == 3
|
|
||||||
assert all(s > 0 for s in spacing)
|
|
||||||
Reference in New Issue
Block a user