slice3d 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
slice3d/__init__.py ADDED
@@ -0,0 +1,46 @@
1
+ """slice3d: 3Dモデルを一定間隔でスライスして断面を出力するライブラリ。
2
+
3
+ 最小の使用例::
4
+
5
+ import slice3d
6
+
7
+ mesh = slice3d.load_mesh("model.stl")
8
+ for s in slice3d.iter_slices(mesh, axis="z", thickness=1.0):
9
+ if not s.is_empty:
10
+ slice3d.save_section(s.path_2d, f"out/{s.index:04d}.svg")
11
+
12
+ または一括処理::
13
+
14
+ import slice3d
15
+
16
+ slice3d.slice_file("model.stl", "out", axis="z", thickness=1.0, fmt="svg")
17
+ """
18
+
19
+ from .core import (
20
+ AXES,
21
+ Slice,
22
+ compute_heights,
23
+ iter_slices,
24
+ load_mesh,
25
+ polylines,
26
+ print_volume,
27
+ save_section,
28
+ slice_file,
29
+ slice_mesh,
30
+ )
31
+
32
+ __version__ = "0.1.0"
33
+
34
+ __all__ = [
35
+ "AXES",
36
+ "Slice",
37
+ "compute_heights",
38
+ "iter_slices",
39
+ "load_mesh",
40
+ "polylines",
41
+ "print_volume",
42
+ "save_section",
43
+ "slice_file",
44
+ "slice_mesh",
45
+ "__version__",
46
+ ]
slice3d/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
slice3d/cli.py ADDED
@@ -0,0 +1,58 @@
1
+ """slice3d のコマンドラインインターフェース。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+
8
+ from .core import AXES, load_mesh, compute_heights, iter_slices, save_section
9
+
10
+
11
+ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
12
+ parser = argparse.ArgumentParser(
13
+ prog="slice3d",
14
+ description="3Dモデルを一定幅でスライスして断面を出力する",
15
+ )
16
+ parser.add_argument("model", type=Path, help="入力3Dモデルファイル (STL/OBJ/PLY/GLBなど)")
17
+ parser.add_argument("--axis", choices=tuple(AXES), default="z", help="スライスする軸 (既定: z)")
18
+ parser.add_argument("--thickness", type=float, default=1.0, help="スライス間隔 [モデル単位] (既定: 1.0)")
19
+ parser.add_argument("--start", type=float, default=None, help="開始位置 (既定: モデルの最小値)")
20
+ parser.add_argument("--end", type=float, default=None, help="終了位置 (既定: モデルの最大値)")
21
+ parser.add_argument("--outdir", type=Path, default=Path("slices"), help="出力先ディレクトリ (既定: ./slices)")
22
+ parser.add_argument(
23
+ "--format",
24
+ choices=("svg", "dxf", "png", "csv"),
25
+ default="svg",
26
+ help="断面の出力形式 (既定: svg)",
27
+ )
28
+ return parser.parse_args(argv)
29
+
30
+
31
+ def main(argv: list[str] | None = None) -> None:
32
+ args = parse_args(argv)
33
+
34
+ mesh = load_mesh(args.model)
35
+ if not mesh.is_watertight:
36
+ print(f"警告: メッシュが閉じていません (is_watertight=False)。断面が欠ける場合があります: {args.model}")
37
+
38
+ heights = compute_heights(mesh, args.axis, args.thickness, args.start, args.end)
39
+ print(f"{len(heights)} 枚の断面を axis={args.axis}, thickness={args.thickness} で生成します")
40
+
41
+ args.outdir.mkdir(parents=True, exist_ok=True)
42
+ stem = args.model.stem
43
+
44
+ n_ok = 0
45
+ for s in iter_slices(mesh, axis=args.axis, thickness=args.thickness, start=args.start, end=args.end):
46
+ if s.is_empty:
47
+ print(f" [{s.index:04d}] {args.axis}={s.position:.4f}: 交差なし (スキップ)")
48
+ continue
49
+ outpath = args.outdir / f"{stem}_{args.axis}{s.index:04d}_{s.position:.4f}.{args.format}"
50
+ save_section(s.path_2d, outpath, args.format)
51
+ print(f" [{s.index:04d}] {args.axis}={s.position:.4f}: {outpath.name}")
52
+ n_ok += 1
53
+
54
+ print(f"完了: {n_ok}/{len(heights)} 断面を {args.outdir} に出力しました")
55
+
56
+
57
+ if __name__ == "__main__":
58
+ main()
slice3d/core.py ADDED
@@ -0,0 +1,247 @@
1
+ """3Dメッシュを一定間隔でスライスし、断面(2Dパス)を得るためのコア機能。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Iterator
8
+
9
+ import numpy as np
10
+ import trimesh
11
+
12
+ __all__ = [
13
+ "AXES",
14
+ "Slice",
15
+ "load_mesh",
16
+ "compute_heights",
17
+ "iter_slices",
18
+ "slice_mesh",
19
+ "save_section",
20
+ "slice_file",
21
+ "print_volume",
22
+ "polylines",
23
+ ]
24
+
25
+ AXES = {"x": 0, "y": 1, "z": 2}
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class Slice:
30
+ """1枚の断面を表す。"""
31
+
32
+ index: int
33
+ axis: str
34
+ position: float
35
+ path_2d: "trimesh.path.Path2D | None"
36
+ # 断面のワールド座標での点列(Nx3配列のリスト)。png保存時に実寸スケールを
37
+ # 揃えるために使う(path_2d はtrimeshが断面ごとに選ぶローカル2D座標系のため、
38
+ # 複数断面間でスケールが揃わない)。
39
+ polylines_3d: list = field(default_factory=list)
40
+
41
+ @property
42
+ def is_empty(self) -> bool:
43
+ return self.path_2d is None
44
+
45
+
46
+ def load_mesh(path: str | Path) -> trimesh.Trimesh:
47
+ """3Dモデルファイルを読み込み、単一の Trimesh として返す。
48
+
49
+ STL / OBJ / PLY / GLB / GLTF など、trimesh が対応する形式を扱える。
50
+ """
51
+ mesh = trimesh.load(path, force="mesh")
52
+ if not isinstance(mesh, trimesh.Trimesh):
53
+ raise TypeError(f"単一のメッシュとして読み込めませんでした: {path}")
54
+ return mesh
55
+
56
+
57
+ def compute_heights(
58
+ mesh: trimesh.Trimesh,
59
+ axis: str,
60
+ thickness: float,
61
+ start: float | None = None,
62
+ end: float | None = None,
63
+ ) -> np.ndarray:
64
+ """指定した軸に沿って、一定間隔(thickness)でスライスする位置の配列を計算する。"""
65
+ if axis not in AXES:
66
+ raise ValueError(f"axis は {list(AXES)} のいずれかである必要があります: {axis!r}")
67
+ if thickness <= 0:
68
+ raise ValueError("thickness は正の値である必要があります")
69
+
70
+ axis_idx = AXES[axis]
71
+ bounds_min, bounds_max = mesh.bounds[:, axis_idx]
72
+ lo = bounds_min if start is None else start
73
+ hi = bounds_max if end is None else end
74
+ if hi <= lo:
75
+ raise ValueError(f"終了位置は開始位置より大きい必要があります (start={lo}, end={hi})")
76
+
77
+ n = int(np.floor((hi - lo) / thickness)) + 1
78
+ return lo + np.arange(n) * thickness
79
+
80
+
81
+ def iter_slices(
82
+ mesh: trimesh.Trimesh,
83
+ axis: str = "z",
84
+ thickness: float = 1.0,
85
+ start: float | None = None,
86
+ end: float | None = None,
87
+ ) -> Iterator[Slice]:
88
+ """メッシュを一定間隔でスライスし、Sliceオブジェクトを1枚ずつ生成する。
89
+
90
+ 交差しない位置では ``path_2d=None`` の Slice を返す(呼び出し側でスキップ判定できる)。
91
+ """
92
+ axis_idx = AXES[axis]
93
+ heights = compute_heights(mesh, axis, thickness, start, end)
94
+
95
+ normal = np.zeros(3)
96
+ normal[axis_idx] = 1.0
97
+ origin = mesh.bounds[0].copy()
98
+
99
+ for i, h in enumerate(heights):
100
+ plane_origin = origin.copy()
101
+ plane_origin[axis_idx] = h
102
+ section = mesh.section(plane_origin=plane_origin, plane_normal=normal)
103
+ if section is None:
104
+ yield Slice(index=i, axis=axis, position=float(h), path_2d=None)
105
+ continue
106
+ path_2d, _transform = section.to_2D()
107
+ yield Slice(
108
+ index=i,
109
+ axis=axis,
110
+ position=float(h),
111
+ path_2d=path_2d,
112
+ polylines_3d=polylines(section),
113
+ )
114
+
115
+
116
+ def slice_mesh(
117
+ mesh: trimesh.Trimesh,
118
+ axis: str = "z",
119
+ thickness: float = 1.0,
120
+ start: float | None = None,
121
+ end: float | None = None,
122
+ ) -> list[Slice]:
123
+ """iter_slices の結果をリストとして返す。"""
124
+ return list(iter_slices(mesh, axis=axis, thickness=thickness, start=start, end=end))
125
+
126
+
127
+ def polylines(path) -> list[np.ndarray]:
128
+ """Path2D/Path3D の全エンティティを点列(Nx2 または Nx3 配列)のリストとして取り出す。
129
+
130
+ ``path.discrete`` は閉じたループしか返さないため、非watertightなメッシュの
131
+ 断面のように閉じていない(開いた)線が含まれていると取りこぼしてしまう。
132
+ この関数は entity 単位で ``entity.discrete(path.vertices)`` を呼ぶことで、
133
+ 開いた線も含めてすべての線分を取得する。
134
+ """
135
+ return [entity.discrete(path.vertices) for entity in path.entities]
136
+
137
+
138
+ def save_section(
139
+ path_2d: "trimesh.path.Path2D",
140
+ outpath: str | Path,
141
+ fmt: str | None = None,
142
+ *,
143
+ polylines_3d: "list[np.ndarray] | None" = None,
144
+ axis: str | None = None,
145
+ bounds: "np.ndarray | None" = None,
146
+ ) -> Path:
147
+ """1枚の断面 (Path2D) をファイルへ保存する。
148
+
149
+ fmt: "svg" | "dxf" | "png" | "csv"。省略時は outpath の拡張子から判定する。
150
+
151
+ fmt="png" で保存する場合は、``polylines_3d``(ワールド座標の点列。
152
+ ``Slice.polylines_3d`` を渡す)、``axis``、``bounds``(``mesh.bounds``)を
153
+ 指定すると、画像の表示範囲をモデル全体のバウンディングボックスに固定できる。
154
+ これらを省略すると各断面の内容だけに自動フィットして保存されるため、
155
+ 断面ごとに画像の縮尺(見た目の大きさ)がバラバラになってしまう点に注意。
156
+ """
157
+ outpath = Path(outpath)
158
+ fmt = (fmt or outpath.suffix.lstrip(".")).lower()
159
+
160
+ if fmt in ("svg", "dxf"):
161
+ data = path_2d.export(file_type=fmt)
162
+ if isinstance(data, str):
163
+ outpath.write_text(data)
164
+ else:
165
+ outpath.write_bytes(data)
166
+ elif fmt == "png":
167
+ import matplotlib.pyplot as plt
168
+
169
+ fig, ax = plt.subplots()
170
+ if polylines_3d is not None and axis is not None and bounds is not None:
171
+ # ワールド座標をそのまま2軸に投影し、表示範囲をモデル全体のバウンディング
172
+ # ボックスに固定する。これにより、断面の大きさに関わらずどの画像も
173
+ # 元モデルに対する実際の縮尺(比率)を保って保存される。
174
+ axis_idx = AXES[axis]
175
+ other = [i for i in range(3) if i != axis_idx]
176
+ for polyline in polylines_3d:
177
+ ax.plot(polyline[:, other[0]], polyline[:, other[1]], "-k")
178
+ ax.set_xlim(bounds[0, other[0]], bounds[1, other[0]])
179
+ ax.set_ylim(bounds[0, other[1]], bounds[1, other[1]])
180
+ ax.set_aspect("equal")
181
+ ax.axis("off")
182
+ fig.savefig(outpath, dpi=150)
183
+ else:
184
+ # 従来どおり: 断面の内容だけに自動フィットする(スケールは断面ごとに異なる)。
185
+ for polyline in polylines(path_2d):
186
+ ax.plot(polyline[:, 0], polyline[:, 1], "-k")
187
+ ax.set_aspect("equal")
188
+ ax.axis("off")
189
+ fig.savefig(outpath, bbox_inches="tight", dpi=150)
190
+ plt.close(fig)
191
+ elif fmt == "csv":
192
+ lines = ["polyline_id,x,y"]
193
+ for i, polyline in enumerate(polylines(path_2d)):
194
+ for x, y in polyline:
195
+ lines.append(f"{i},{x},{y}")
196
+ outpath.write_text("\n".join(lines))
197
+ else:
198
+ raise ValueError(f"未対応の出力形式です: {fmt!r} (対応形式: svg, dxf, png, csv)")
199
+
200
+ return outpath
201
+
202
+
203
+ def print_volume(mesh: trimesh.Trimesh) -> float:
204
+ """メッシュの体積を標準出力に表示し、その値を返す。
205
+
206
+ メッシュが水密(watertight)でない場合、体積の計算結果が不正確になりうるため警告を表示する。
207
+ """
208
+ if not mesh.is_watertight:
209
+ print("警告: メッシュが水密でないため、体積の計算結果は不正確な可能性があります")
210
+
211
+ volume = float(mesh.volume)
212
+ print(f"体積: {volume}")
213
+ return volume
214
+
215
+
216
+ def slice_file(
217
+ model_path: str | Path,
218
+ outdir: str | Path,
219
+ axis: str = "z",
220
+ thickness: float = 1.0,
221
+ start: float | None = None,
222
+ end: float | None = None,
223
+ fmt: str = "svg",
224
+ ) -> list[Path]:
225
+ """3Dモデルファイルを読み込み、スライスして outdir へ一括出力する。
226
+
227
+ 戻り値は書き出したファイルパスのリスト(交差しないスライスはスキップされる)。
228
+ """
229
+ model_path = Path(model_path)
230
+ outdir = Path(outdir)
231
+ outdir.mkdir(parents=True, exist_ok=True)
232
+
233
+ mesh = load_mesh(model_path)
234
+ stem = model_path.stem
235
+
236
+ written: list[Path] = []
237
+ for s in iter_slices(mesh, axis=axis, thickness=thickness, start=start, end=end):
238
+ if s.is_empty:
239
+ continue
240
+ outpath = outdir / f"{stem}_{axis}{s.index:04d}_{s.position:.4f}.{fmt}"
241
+ written.append(
242
+ save_section(
243
+ s.path_2d, outpath, fmt,
244
+ polylines_3d=s.polylines_3d, axis=axis, bounds=mesh.bounds,
245
+ )
246
+ )
247
+ return written
slice3d/gui.py ADDED
@@ -0,0 +1,471 @@
1
+ """3Dモデルの断面・体積を確認するGUIビューア。
2
+
3
+ 専用ウィンドウを開き、3D表示でモデル全体と現在の切断位置を確認しながら、
4
+ スライダーで断面を切り替えられる。軸(x/y/z)とスライス間隔(thickness)はウィンドウ上で変更できる。
5
+
6
+ 使い方::
7
+
8
+ slice3d-gui model.stl
9
+ slice3d-gui model.stl --axis x --thickness 0.01
10
+ slice3d-gui model.glb # glTF/GLBは仕様上メートル単位なので自動で"m"表示
11
+ slice3d-gui model.stl --units mm # STLなど単位不明な形式は明示的に指定
12
+
13
+ matplotlib が必要 (``pip install "slice3d[gui]"``)。
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import math
20
+ from pathlib import Path
21
+
22
+ import numpy as np
23
+ import trimesh
24
+ import matplotlib.pyplot as plt
25
+ from matplotlib.widgets import Button, RadioButtons, Slider, TextBox
26
+ from mpl_toolkits.mplot3d.art3d import Poly3DCollection
27
+
28
+ from . import core
29
+
30
+
31
+ def _pick_cjk_font() -> str | None:
32
+ """環境にインストールされているCJK対応フォントを1つ選ぶ(文字化け防止用)。"""
33
+ import matplotlib.font_manager as fm
34
+
35
+ available = {f.name for f in fm.fontManager.ttflist}
36
+ for candidate in ("Hiragino Sans", "Yu Gothic", "Meiryo", "Noto Sans CJK JP", "IPAexGothic"):
37
+ if candidate in available:
38
+ return candidate
39
+ return None
40
+
41
+
42
+ _cjk_font = _pick_cjk_font()
43
+ if _cjk_font:
44
+ plt.rcParams["font.family"] = [_cjk_font, "DejaVu Sans"]
45
+ plt.rcParams["axes.unicode_minus"] = False
46
+
47
+ # 3Dプレビューが重くならないよう、面数がこれを超えたらメッシュを簡略化して表示する。
48
+ # mplot3dは面数に比例して回転・再描画が遅くなるため、数千面程度に抑えるとスムーズに動く。
49
+ # (スライス計算自体は元メッシュの精度で行われ、この簡略化は3D表示にのみ影響する)
50
+ _MAX_PREVIEW_FACES = 4_000
51
+
52
+ # glTF/GLBは仕様上メートル単位と定められているため、既定値として表示に使う。
53
+ # STL/OBJ/PLYなどは単位の情報を持たないファイル形式なので、既定は単位なし(空文字)。
54
+ _UNITS_BY_EXTENSION = {".glb": "m", ".gltf": "m"}
55
+
56
+ _AXIS_NAMES = ("x", "y", "z")
57
+ _FORMATS = ("svg", "png", "dxf", "csv")
58
+
59
+
60
+ def _default_units(model_path: Path) -> str:
61
+ return _UNITS_BY_EXTENSION.get(model_path.suffix.lower(), "")
62
+
63
+
64
+ def _nice_thickness(extent: float, target_slices: int = 30) -> float:
65
+ """extentを約target_slices分割する、キリの良いthickness値(1/2/5 x 10^n)を選ぶ。"""
66
+ raw = extent / target_slices
67
+ if raw <= 0:
68
+ return max(raw, 1e-9)
69
+ magnitude = 10 ** math.floor(math.log10(raw))
70
+ for m in (1, 2, 5, 10):
71
+ candidate = m * magnitude
72
+ if candidate >= raw:
73
+ return candidate
74
+ return 10 * magnitude
75
+
76
+
77
+ class SliceViewer:
78
+ """3D表示 + スライダーで断面を切り替えながら確認するmatplotlibウィンドウ。"""
79
+
80
+ def __init__(
81
+ self,
82
+ model_path: str | Path,
83
+ axis: str = "z",
84
+ thickness: float | None = None,
85
+ max_preview_faces: int = _MAX_PREVIEW_FACES,
86
+ units: str | None = None,
87
+ fmt: str = "svg",
88
+ ):
89
+ self.model_path = Path(model_path)
90
+ self.mesh = core.load_mesh(self.model_path)
91
+ self.axis = axis
92
+ self.volume = float(self.mesh.volume)
93
+ self.max_preview_faces = max_preview_faces
94
+ self.units = _default_units(self.model_path) if units is None else units
95
+ if fmt not in _FORMATS:
96
+ raise ValueError(f"fmt は {list(_FORMATS)} のいずれかである必要があります: {fmt!r}")
97
+ self.format = fmt
98
+ self._format_buttons: dict[str, Button] = {}
99
+
100
+ if thickness is None:
101
+ extent = self.mesh.extents[core.AXES[axis]]
102
+ thickness = _nice_thickness(extent)
103
+ self.thickness = thickness
104
+
105
+ self.heights = None
106
+ self.slider = None
107
+ self._plane_artist = None
108
+ self._curve_lines: list = []
109
+
110
+ self._build_ui()
111
+ self._recompute_slices()
112
+
113
+ # ---- 表示用フォーマット ----
114
+ # 有効数字3桁に丸める(生の浮動小数点をそのまま出すと桁数が多すぎて読みにくいため)。
115
+ def _fmt_length(self, value: float) -> str:
116
+ suffix = f" {self.units}" if self.units else ""
117
+ return f"{value:.3g}{suffix}"
118
+
119
+ def _fmt_volume(self, value: float) -> str:
120
+ suffix = f" {self.units}³" if self.units else ""
121
+ return f"{value:.3g}{suffix}"
122
+
123
+ # ---- データ ----
124
+ def _recompute_slices(self) -> None:
125
+ self.heights = core.compute_heights(self.mesh, self.axis, self.thickness)
126
+ self._rebuild_slider()
127
+
128
+ def _section_at(self, index: int):
129
+ """指定インデックスの断面を計算する。(path_2d, discrete_3d) を返す。
130
+
131
+ path_2d は2D平面上のPath2D(交差なしならNone)、discrete_3d は
132
+ 3Dプレビュー用のポリライン(Nx3配列)のリスト。
133
+
134
+ polylines()(core.pyのentityベースの抽出)を使うことで、非watertightな
135
+ メッシュの断面のように閉じていない(開いた)線も欠落なく拾う。
136
+ ``section.discrete`` は閉じたループしか返さないため使わない。
137
+ """
138
+ axis_idx = core.AXES[self.axis]
139
+ normal = [0.0, 0.0, 0.0]
140
+ normal[axis_idx] = 1.0
141
+ origin = self.mesh.bounds[0].copy()
142
+ origin[axis_idx] = self.heights[index]
143
+ section = self.mesh.section(plane_origin=origin, plane_normal=normal)
144
+ if section is None:
145
+ return None, []
146
+ path_2d, _transform = section.to_2D()
147
+ return path_2d, core.polylines(section)
148
+
149
+ def _plane_corners(self, index: int) -> np.ndarray:
150
+ axis_idx = core.AXES[self.axis]
151
+ h = self.heights[index]
152
+ other = [i for i in range(3) if i != axis_idx]
153
+ b = self.mesh.bounds
154
+ lo0, hi0 = b[0, other[0]], b[1, other[0]]
155
+ lo1, hi1 = b[0, other[1]], b[1, other[1]]
156
+ corners = np.zeros((4, 3))
157
+ for k, (a, c) in enumerate(((lo0, lo1), (hi0, lo1), (hi0, hi1), (lo0, hi1))):
158
+ corners[k, axis_idx] = h
159
+ corners[k, other[0]] = a
160
+ corners[k, other[1]] = c
161
+ return corners
162
+
163
+ # ---- UI構築 ----
164
+ def _build_ui(self) -> None:
165
+ self.fig = plt.figure(figsize=(11, 9))
166
+ try:
167
+ self.fig.canvas.manager.set_window_title(f"slice3d viewer - {self.model_path.name}")
168
+ except AttributeError:
169
+ pass
170
+
171
+ self.ax_3d = self.fig.add_axes((0.03, 0.34, 0.45, 0.55), projection="3d")
172
+ self._draw_static_mesh()
173
+
174
+ self.ax_section = self.fig.add_axes((0.55, 0.34, 0.43, 0.55))
175
+ self.ax_section.set_aspect("equal")
176
+ self.ax_section.set_title("Cross-section (view along slicing axis)", fontsize=10)
177
+
178
+ watertight_note = "" if self.mesh.is_watertight else " (non-watertight: volume is approximate)"
179
+ self.fig.text(
180
+ 0.02, 0.975,
181
+ f"{self.model_path.name} volume = {self._fmt_volume(self.volume)}{watertight_note}",
182
+ fontsize=10, va="top",
183
+ )
184
+ self.pos_text = self.fig.text(0.02, 0.95, "", fontsize=10, va="top")
185
+ self.fig.text(
186
+ 0.03, 0.92, "Full model (red = current slice plane)", fontsize=10, va="top"
187
+ )
188
+
189
+ # ラジオボタンは既定だと丸のサイズ・クリック領域が小さく押しづらいため、
190
+ # 専用エリアを広めに取った上で radio_props/label_props でマーカーと
191
+ # フォントを拡大している。
192
+ ax_radio = self.fig.add_axes((0.01, 0.02, 0.13, 0.27))
193
+ ax_radio.set_title("Axis", fontsize=11)
194
+ self.radio = RadioButtons(
195
+ ax_radio,
196
+ ("x", "y", "z"),
197
+ active="xyz".index(self.axis),
198
+ radio_props={"s": 160},
199
+ label_props={"fontsize": [14, 14, 14]},
200
+ )
201
+ self.radio.on_clicked(self._on_axis_change)
202
+
203
+ self.ax_slider = self.fig.add_axes((0.19, 0.25, 0.71, 0.05))
204
+ # スライダーのつまみも既定サイズだと掴みづらいので大きくする
205
+ # (トラック自体はどこをクリックしてもその位置へ移動できる)。
206
+ self._slider_handle_style = {"size": 18}
207
+
208
+ thickness_label = f"Thickness ({self.units}) " if self.units else "Thickness "
209
+ ax_thickness = self.fig.add_axes((0.19, 0.16, 0.18, 0.06))
210
+ self.thickness_box = TextBox(ax_thickness, thickness_label, initial=f"{self.thickness:g}")
211
+ self.thickness_box.text_disp.set_fontsize(12)
212
+ self.thickness_box.label.set_fontsize(11)
213
+ self.thickness_box.on_submit(self._on_thickness_submit)
214
+
215
+ ax_save_current = self.fig.add_axes((0.62, 0.16, 0.28, 0.06))
216
+ self.save_current_button = Button(ax_save_current, "")
217
+ self.save_current_button.label.set_fontsize(11)
218
+ self.save_current_button.on_clicked(self._on_save)
219
+
220
+ self.fig.text(0.19, 0.10, "Format:", fontsize=10, va="center")
221
+ format_x = (0.30, 0.44, 0.58, 0.72)
222
+ format_width = 0.13
223
+ for x, fmt in zip(format_x, _FORMATS):
224
+ ax_fmt = self.fig.add_axes((x, 0.075, format_width, 0.05))
225
+ button = Button(ax_fmt, fmt.upper())
226
+ button.label.set_fontsize(10)
227
+ button.on_clicked(lambda event, f=fmt: self._on_format_change(f))
228
+ self._format_buttons[fmt] = button
229
+
230
+ ax_save_all = self.fig.add_axes((0.19, 0.005, 0.81, 0.06))
231
+ self.save_all_button = Button(ax_save_all, "")
232
+ self.save_all_button.label.set_fontsize(11)
233
+ self.save_all_button.on_clicked(self._on_save_all)
234
+
235
+ self._refresh_format_buttons()
236
+
237
+ def _preview_mesh(self):
238
+ """3D表示専用の軽量化されたメッシュを返す(スライス計算には使わない)。"""
239
+ mesh = self.mesh
240
+ if len(mesh.faces) <= self.max_preview_faces:
241
+ return mesh
242
+ try:
243
+ return mesh.simplify_quadric_decimation(face_count=self.max_preview_faces)
244
+ except Exception:
245
+ # fast_simplification が無い環境などへのフォールバック: 単純間引き。
246
+ # 形状の見え方は粗くなるが、表示が固まるよりはよい。
247
+ step = math.ceil(len(mesh.faces) / self.max_preview_faces)
248
+ faces = mesh.faces[::step]
249
+ return trimesh.Trimesh(vertices=mesh.vertices, faces=faces, process=False)
250
+
251
+ def _draw_static_mesh(self) -> None:
252
+ """モデル全体を半透明の3Dサーフェスとして一度だけ描画する(スライス操作では再描画しない)。"""
253
+ preview = self._preview_mesh()
254
+ tri = preview.vertices[preview.faces]
255
+
256
+ surface = Poly3DCollection(tri, alpha=0.25, facecolor="lightsteelblue", edgecolor="none")
257
+ self.ax_3d.add_collection3d(surface)
258
+
259
+ b = self.mesh.bounds
260
+ self.ax_3d.set_xlim(b[0, 0], b[1, 0])
261
+ self.ax_3d.set_ylim(b[0, 1], b[1, 1])
262
+ self.ax_3d.set_zlim(b[0, 2], b[1, 2])
263
+ extents = np.maximum(self.mesh.extents, 1e-9)
264
+ self.ax_3d.set_box_aspect(tuple(extents))
265
+ self.ax_3d.set_xlabel("x", fontsize=8)
266
+ self.ax_3d.set_ylabel("y", fontsize=8)
267
+ self.ax_3d.set_zlabel("z", fontsize=8)
268
+
269
+ def _update_3d_highlight(self, index: int, discrete_3d) -> None:
270
+ if self._plane_artist is not None:
271
+ self._plane_artist.remove()
272
+ self._plane_artist = None
273
+ for line in self._curve_lines:
274
+ line.remove()
275
+ self._curve_lines = []
276
+
277
+ corners = self._plane_corners(index)
278
+ plane = Poly3DCollection([corners], alpha=0.15, facecolor="tomato", edgecolor="none")
279
+ self.ax_3d.add_collection3d(plane)
280
+ self._plane_artist = plane
281
+
282
+ for polyline in discrete_3d:
283
+ (line,) = self.ax_3d.plot(
284
+ polyline[:, 0], polyline[:, 1], polyline[:, 2], color="red", linewidth=2
285
+ )
286
+ self._curve_lines.append(line)
287
+
288
+ def _rebuild_slider(self) -> None:
289
+ n = len(self.heights)
290
+ current = 0 if self.slider is None else min(int(self.slider.val), n - 1)
291
+ self.ax_slider.clear()
292
+ self.slider = Slider(
293
+ self.ax_slider,
294
+ "slice",
295
+ 0,
296
+ max(n - 1, 0),
297
+ valinit=current,
298
+ valstep=1,
299
+ handle_style=self._slider_handle_style,
300
+ )
301
+ self.slider.label.set_fontsize(11)
302
+ self.slider.on_changed(lambda val: self._show_index(int(val)))
303
+ self._show_index(current)
304
+
305
+ # ---- コールバック ----
306
+ def _on_axis_change(self, label: str) -> None:
307
+ self.axis = label
308
+ self.thickness = _nice_thickness(self.mesh.extents[core.AXES[label]])
309
+ self.thickness_box.set_val(f"{self.thickness:g}")
310
+ self._recompute_slices()
311
+ self._refresh_format_buttons() # Save Allボタンのラベルに軸名が含まれるため更新
312
+ self.fig.canvas.draw_idle()
313
+
314
+ def _on_thickness_submit(self, text: str) -> None:
315
+ try:
316
+ thickness = float(text)
317
+ except ValueError:
318
+ return
319
+ if thickness <= 0:
320
+ return
321
+ self.thickness = thickness
322
+ self._recompute_slices()
323
+ self.fig.canvas.draw_idle()
324
+
325
+ def _on_format_change(self, fmt: str) -> None:
326
+ self.format = fmt
327
+ self._refresh_format_buttons()
328
+ self.fig.canvas.draw_idle()
329
+
330
+ def _refresh_format_buttons(self) -> None:
331
+ """選択中のフォーマットのボタンをハイライトし、保存ボタンのラベルを更新する。"""
332
+ for fmt, button in self._format_buttons.items():
333
+ selected = fmt == self.format
334
+ button.color = "lightblue" if selected else "0.85"
335
+ button.hovercolor = "skyblue" if selected else "0.95"
336
+ button.ax.set_facecolor(button.color)
337
+ self.save_current_button.label.set_text(f"Save Current Slice ({self.format.upper()})")
338
+ self.save_all_button.label.set_text(f"Save All Slices Along {self.axis.upper()}-Axis ({self.format.upper()})")
339
+
340
+ def _outdir(self) -> Path:
341
+ outdir = self.model_path.parent / "slices_gui"
342
+ outdir.mkdir(exist_ok=True)
343
+ return outdir
344
+
345
+ def _on_save(self, event) -> None:
346
+ index = int(self.slider.val)
347
+ path_2d, discrete_3d = self._section_at(index)
348
+ if path_2d is None:
349
+ print("Cannot save: no intersection at this slice position")
350
+ return
351
+ outdir = self._outdir()
352
+ outpath = outdir / f"{self.model_path.stem}_{self.axis}{index:04d}_{self.heights[index]:.4f}.{self.format}"
353
+ # polylines_3d/axis/bounds を渡すことで、保存画像(png)の表示範囲を
354
+ # モデル全体のバウンディングボックスに固定する(断面ごとに縮尺がバラつかない)。
355
+ core.save_section(
356
+ path_2d, outpath, self.format,
357
+ polylines_3d=discrete_3d, axis=self.axis, bounds=self.mesh.bounds,
358
+ )
359
+ print(f"Saved: {outpath}")
360
+
361
+ def _on_save_all(self, event) -> None:
362
+ """現在の軸・thickness設定で全スライスを一括保存する(交差しない位置はスキップ)。"""
363
+ outdir = self._outdir()
364
+ written = core.slice_file(
365
+ self.model_path,
366
+ outdir,
367
+ axis=self.axis,
368
+ thickness=self.thickness,
369
+ fmt=self.format,
370
+ )
371
+ print(
372
+ f"Saved {len(written)}/{len(self.heights)} slices "
373
+ f"(axis={self.axis}, thickness={self._fmt_length(self.thickness)}, format={self.format}) to {outdir}"
374
+ )
375
+
376
+ # ---- 描画 ----
377
+ def _show_index(self, index: int) -> None:
378
+ index = max(0, min(index, len(self.heights) - 1))
379
+ position = self.heights[index]
380
+ path_2d, discrete_3d = self._section_at(index)
381
+
382
+ self._update_3d_highlight(index, discrete_3d)
383
+
384
+ # 断面図は3Dハイライトと同じワールド座標(discrete_3d)をそのまま2軸に投影して描く。
385
+ # path_2d(trimeshが断面ごとに独自に選ぶローカル2D座標系)を使うと、断面の形だけで
386
+ # 自動スケーリングされてしまい、小さな断片がパネルいっぱいに拡大されて3D表示と
387
+ # 対応が取れなくなる(特にthin方向の軸でスライスすると顕著)。
388
+ # ここではモデル全体のバウンディングボックスに表示範囲を固定し、常に同じ縮尺で見せる。
389
+ axis_idx = core.AXES[self.axis]
390
+ other = [i for i in range(3) if i != axis_idx]
391
+ xlabel, ylabel = _AXIS_NAMES[other[0]], _AXIS_NAMES[other[1]]
392
+
393
+ self.ax_section.clear()
394
+ self.ax_section.set_aspect("equal")
395
+ self.ax_section.set_title("Cross-section (view along slicing axis)", fontsize=10)
396
+ self.ax_section.set_xlabel(xlabel, fontsize=9)
397
+ self.ax_section.set_ylabel(ylabel, fontsize=9)
398
+ b = self.mesh.bounds
399
+ self.ax_section.set_xlim(b[0, other[0]], b[1, other[0]])
400
+ self.ax_section.set_ylim(b[0, other[1]], b[1, other[1]])
401
+ if not discrete_3d:
402
+ self.ax_section.text(
403
+ 0.5, 0.5, "No intersection", ha="center", va="center", transform=self.ax_section.transAxes
404
+ )
405
+ else:
406
+ for polyline in discrete_3d:
407
+ self.ax_section.plot(polyline[:, other[0]], polyline[:, other[1]], "-k")
408
+
409
+ self.pos_text.set_text(
410
+ f"axis={self.axis} thickness={self._fmt_length(self.thickness)} "
411
+ f"slice={index + 1}/{len(self.heights)} position={self._fmt_length(position)}"
412
+ )
413
+ self.fig.canvas.draw_idle()
414
+
415
+ def show(self) -> None:
416
+ plt.show()
417
+
418
+
419
+ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
420
+ parser = argparse.ArgumentParser(
421
+ prog="slice3d-gui", description="Interactive GUI viewer for a 3D model's volume and slices"
422
+ )
423
+ parser.add_argument("model", type=Path, help="input 3D model file (STL/OBJ/PLY/GLB, etc.)")
424
+ parser.add_argument("--axis", choices=tuple(core.AXES), default="z", help="initial slicing axis")
425
+ parser.add_argument(
426
+ "--thickness",
427
+ type=float,
428
+ default=None,
429
+ help='initial slice spacing (a.k.a. "slice thickness"); '
430
+ "default: a round number giving roughly 30 slices across the model",
431
+ )
432
+ parser.add_argument(
433
+ "--units",
434
+ type=str,
435
+ default=None,
436
+ help='length unit label to display next to values, e.g. "m" or "mm". '
437
+ 'Defaults to "m" for glTF/GLB files (meters by spec) and no unit otherwise; '
438
+ 'pass --units "" to force no unit label.',
439
+ )
440
+ parser.add_argument(
441
+ "--max-preview-faces",
442
+ type=int,
443
+ default=_MAX_PREVIEW_FACES,
444
+ help=f"max face count for the 3D preview mesh (default: {_MAX_PREVIEW_FACES}); "
445
+ "lower this if the 3D view feels slow",
446
+ )
447
+ parser.add_argument(
448
+ "--format",
449
+ choices=_FORMATS,
450
+ default="svg",
451
+ help="initial export format for the Save buttons (default: svg); "
452
+ "can also be changed in the window",
453
+ )
454
+ return parser.parse_args(argv)
455
+
456
+
457
+ def main(argv: list[str] | None = None) -> None:
458
+ args = parse_args(argv)
459
+ viewer = SliceViewer(
460
+ args.model,
461
+ axis=args.axis,
462
+ thickness=args.thickness,
463
+ max_preview_faces=args.max_preview_faces,
464
+ units=args.units,
465
+ fmt=args.format,
466
+ )
467
+ viewer.show()
468
+
469
+
470
+ if __name__ == "__main__":
471
+ main()
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.5
2
+ Name: slice3d
3
+ Version: 0.1.0
4
+ Summary: 3Dモデルを一定間隔でスライスして断面(輪郭)を出力するライブラリ/CLI
5
+ Project-URL: Homepage, https://github.com/Hanibuchi/slice-3d
6
+ Project-URL: Issues, https://github.com/Hanibuchi/slice-3d/issues
7
+ Author-email: hanibuchi <hanitech8686@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: 3d,cad,cross-section,mesh,slicing,stl,trimesh
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
23
+ Classifier: Topic :: Scientific/Engineering
24
+ Requires-Python: >=3.9
25
+ Requires-Dist: networkx>=3.0
26
+ Requires-Dist: numpy>=1.24
27
+ Requires-Dist: scipy>=1.10
28
+ Requires-Dist: shapely>=2.0
29
+ Requires-Dist: trimesh>=4.0.0
30
+ Provides-Extra: dev
31
+ Requires-Dist: fast-simplification>=0.1.7; extra == 'dev'
32
+ Requires-Dist: matplotlib!=3.11.*,>=3.8; extra == 'dev'
33
+ Requires-Dist: pytest>=7.0; extra == 'dev'
34
+ Provides-Extra: gui
35
+ Requires-Dist: fast-simplification>=0.1.7; extra == 'gui'
36
+ Requires-Dist: matplotlib!=3.11.*,>=3.8; extra == 'gui'
37
+ Provides-Extra: viz
38
+ Requires-Dist: matplotlib>=3.7; extra == 'viz'
39
+ Description-Content-Type: text/markdown
40
+
41
+ # slice3d
42
+
43
+ 3Dモデル(STL / OBJ / PLY / GLB / GLTF など)を一定間隔でスライスし、断面(輪郭)を SVG / DXF / PNG / CSV として出力するPythonライブラリ・CLIです。[trimesh](https://github.com/mikedh/trimesh) をベースにしています。
44
+
45
+ アンモナイトの化石モデルをX軸方向にスライスした例:
46
+
47
+ ![スライス一覧](docs/assets/ammonite_slices_grid.png)
48
+
49
+ 中心付近の断面(渦巻き状の房室がきれいに現れる):
50
+
51
+ ![中心断面](docs/assets/ammonite_center_slice.png)
52
+
53
+ ## インストール
54
+
55
+ ```bash
56
+ pip install slice3d
57
+
58
+ # PNG出力を使う場合(matplotlibが必要)
59
+ pip install "slice3d[viz]"
60
+ ```
61
+
62
+ 開発時はこのリポジトリを editable インストールしてください。
63
+
64
+ ```bash
65
+ python -m venv .venv && source .venv/bin/activate
66
+ pip install -e ".[dev]"
67
+ ```
68
+
69
+ ## CLIとして使う
70
+
71
+ ```bash
72
+ slice3d model.stl --axis z --thickness 2.0 --outdir slices --format svg
73
+ ```
74
+
75
+ | オプション | 説明 | 既定値 |
76
+ | --- | --- | --- |
77
+ | `--axis {x,y,z}` | スライスする軸 | `z` |
78
+ | `--thickness` | スライス間隔(モデル単位) | `1.0` |
79
+ | `--start` / `--end` | スライス範囲(省略時はモデルのバウンディングボックス全体) | なし |
80
+ | `--outdir` | 出力先ディレクトリ | `./slices` |
81
+ | `--format {svg,dxf,png,csv}` | 断面の出力形式 | `svg` |
82
+
83
+ PNG出力(CLI/`slice_file`/GUIいずれも共通)は、断面ごとの内容に自動フィットするのではなく、元モデル全体のバウンディングボックスを基準に表示範囲を固定して保存する。そのため、同じスライス処理で出力した画像はすべて同じピクセルサイズ・同じ縮尺になり、小さな断面ほど小さく、大きな断面ほど大きく写る(元モデルに対する実際の大きさの比率がそのまま画像に反映される)。
84
+
85
+ 交差しない位置のスライスは自動的にスキップされます。
86
+
87
+ ## GUIビューア
88
+
89
+ 体積・スライス位置を確認しながら、軸やスライス間隔(thickness)をその場で変更できる専用ウィンドウを開きます。
90
+
91
+ ```bash
92
+ pip install "slice3d[gui]"
93
+ slice3d-gui model.stl
94
+ slice3d-gui model.stl --axis x --thickness 0.01
95
+ slice3d-gui model.glb # glTF/GLBは仕様上メートル単位なので自動で"m"表示
96
+ slice3d-gui model.stl --units mm # STLなど単位不明な形式は明示的に指定
97
+ slice3d-gui model.stl --format png # Save系ボタンの初期フォーマット(既定: svg)
98
+ ```
99
+
100
+ ![GUIビューア](docs/assets/gui_screenshot.png)
101
+
102
+ ウィンドウ内の表示は(体積・軸・数値など)すべて英語で統一しています。
103
+
104
+ - ウィンドウ上部にモデル名・体積(`mesh.volume`、非watertightなら近似値である旨も表示)
105
+ - 左側にモデル全体を半透明の3D表示。現在の切断位置を赤い平面と断面の輪郭線で重ねて表示するので、どこをどの向きで切っているか一目で分かる
106
+ - 右側にその断面(切断面を真上から見た2D形状)をリアルタイム表示
107
+ - スライダーでスライス位置(index / position)を切り替え
108
+ - `Axis` ラジオボタンでスライス軸を切り替え(切り替え時はthicknessが軸の全長に応じてキリの良い値(1/2/5 × 10ⁿ、約30分割相当)に自動再設定される)
109
+ - `Thickness` テキストボックスで間隔を指定して Enter → 断面数が再計算される
110
+ - 体積・thickness・positionの表示には単位が付く。glTF/GLB(`.glb` / `.gltf`)は仕様上メートル単位と定められているため自動で`m`が付き、STL/OBJ/PLYなど単位情報を持たない形式は既定で単位なし
111
+ - `--units`(例: `mm`, `cm`)で表示単位を明示指定できる。`--units ""` で単位表示を消すことも可能
112
+ - `Format` ボタン(SVG/PNG/DXF/CSV)で保存形式を選択。選択中の形式はハイライトされ、Saveボタンのラベルにも反映される
113
+ - `Save Current Slice` ボタンで現在表示中の断面を1枚保存
114
+ - `Save All Slices Along <軸>-Axis` ボタンで、現在の軸・thickness設定のまま全断面を一括保存(交差しない位置は自動的にスキップ)
115
+ - 保存先はどちらも `<モデルと同じディレクトリ>/slices_gui/`
116
+
117
+ ## ライブラリとして使う
118
+
119
+ ```python
120
+ import slice3d
121
+
122
+ # 一括処理: モデルを読み込んでスライスし、ディレクトリへ書き出す
123
+ written = slice3d.slice_file("model.stl", "out", axis="z", thickness=1.0, fmt="svg")
124
+
125
+ # 細かく制御したい場合
126
+ mesh = slice3d.load_mesh("model.stl")
127
+ for s in slice3d.iter_slices(mesh, axis="z", thickness=1.0):
128
+ if s.is_empty:
129
+ continue
130
+ print(s.index, s.position, len(s.path_2d.discrete))
131
+ slice3d.save_section(s.path_2d, f"out/{s.index:04d}.svg")
132
+ ```
133
+
134
+ ### API
135
+
136
+ - `slice3d.load_mesh(path)` — 3Dモデルを読み込み `trimesh.Trimesh` を返す
137
+ - `slice3d.compute_heights(mesh, axis, thickness, start=None, end=None)` — スライス位置の配列を計算
138
+ - `slice3d.iter_slices(mesh, axis="z", thickness=1.0, start=None, end=None)` — `Slice` を1枚ずつ生成するイテレータ
139
+ - `slice3d.slice_mesh(...)` — `iter_slices` の結果をリストで取得
140
+ - `slice3d.save_section(path_2d, outpath, fmt=None)` — 1枚の断面を保存(`fmt` 省略時は拡張子から推定)
141
+ - `slice3d.print_volume(mesh)` — メッシュの体積を標準出力に表示し、その値を返す
142
+ - `slice3d.slice_file(model_path, outdir, axis="z", thickness=1.0, start=None, end=None, fmt="svg")` — 読み込み〜保存までを一括実行
143
+
144
+ `Slice` は `index`, `axis`, `position`, `path_2d`(`trimesh.path.Path2D | None`), `is_empty` を持つデータクラスです。
145
+
146
+ ## サンプル
147
+
148
+ ```bash
149
+ python examples/slice_ammonite.py
150
+ ```
151
+
152
+ `examples/ammonite.glb` をX軸方向にスライスし、`examples/output/` にPNGを出力します。
153
+
154
+ ## 開発
155
+
156
+ ```bash
157
+ pip install -e ".[dev]"
158
+ pytest
159
+ ```
160
+
161
+ ## ライセンス
162
+
163
+ MIT License. [LICENSE](LICENSE) を参照してください。
@@ -0,0 +1,10 @@
1
+ slice3d/__init__.py,sha256=A7eBDOPPl7Q4fu6DsCwU1jdShb9ZJ6RvFOK67AH7nnw,889
2
+ slice3d/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
3
+ slice3d/cli.py,sha256=SSQFM88Pfj1VPMlCdDgmf-Cu5OooSERvjIUSKjndAr4,2552
4
+ slice3d/core.py,sha256=OCGcoucoRnGGzaVG9vGJ2viu-ie3aTF0kpNFdd9zOuA,8998
5
+ slice3d/gui.py,sha256=x__81vNleECdsB9fHZLwJor8-EaApIkkkjXoia7yD5Y,19700
6
+ slice3d-0.1.0.dist-info/METADATA,sha256=S_u710N4b3KBLagn1LIJN8MFE2i3Q62qIqFuReMawpQ,7757
7
+ slice3d-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
8
+ slice3d-0.1.0.dist-info/entry_points.txt,sha256=k08GJgfEHWTQlWQU767X3pOY5QIgeNhQQoyCvSMjuLU,76
9
+ slice3d-0.1.0.dist-info/licenses/LICENSE,sha256=NCdIQCSLUrkKbtgHzg4Sg45n-dSns0YGb5zHHNciVmo,1066
10
+ slice3d-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ slice3d = slice3d.cli:main
3
+ slice3d-gui = slice3d.gui:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hanibuchi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.