fluxplot 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.
- fluxplot/__init__.py +115 -0
- fluxplot/_fieldmap.py +97 -0
- fluxplot/_mesh_reduce.py +54 -0
- fluxplot/_scene3d_size.py +95 -0
- fluxplot/_viewer/THIRD-PARTY.txt +23 -0
- fluxplot/_viewer/flux-model3d-viewer.min.js +4221 -0
- fluxplot/_viewer/stamp.json +4 -0
- fluxplot/api.py +1196 -0
- fluxplot/autotag.py +164 -0
- fluxplot/base.mplstyle +0 -0
- fluxplot/brackets.py +242 -0
- fluxplot/canonical_json.py +23 -0
- fluxplot/capture.py +150 -0
- fluxplot/colorcheck.py +285 -0
- fluxplot/colors.py +727 -0
- fluxplot/colorscale.py +477 -0
- fluxplot/data.py +178 -0
- fluxplot/definitions/colormaps.json +1639 -0
- fluxplot/definitions/flexoki.tokens.json +2571 -0
- fluxplot/definitions/palettes.json +2547 -0
- fluxplot/descriptors.py +87 -0
- fluxplot/fields.py +611 -0
- fluxplot/fits.py +240 -0
- fluxplot/glb.py +84 -0
- fluxplot/ids.py +173 -0
- fluxplot/images.py +362 -0
- fluxplot/integrity.py +27 -0
- fluxplot/manifest.py +788 -0
- fluxplot/mesh3d.py +376 -0
- fluxplot/panels.py +284 -0
- fluxplot/postprocess.py +638 -0
- fluxplot/presets.py +66 -0
- fluxplot/provenance.py +177 -0
- fluxplot/raster.py +295 -0
- fluxplot/recipe.py +178 -0
- fluxplot/render.py +66 -0
- fluxplot/roles.py +147 -0
- fluxplot/scene3d.py +386 -0
- fluxplot/scene3d_manifest.py +112 -0
- fluxplot/scene3d_viewer.py +633 -0
- fluxplot/schemas/.gitkeep +0 -0
- fluxplot/schemas/manifest.schema.json +2479 -0
- fluxplot/schemas/recipe.schema.json +179 -0
- fluxplot/schemas/scene3d.schema.json +461 -0
- fluxplot/seaborn_adapters.py +323 -0
- fluxplot/signature_fluxplots/__init__.py +18 -0
- fluxplot/signature_fluxplots/_colour.py +412 -0
- fluxplot/signature_fluxplots/fluxbox.py +433 -0
- fluxplot/signature_fluxplots/glowbar.py +769 -0
- fluxplot/signature_fluxplots/hexmatrix.py +927 -0
- fluxplot/stats/__init__.py +63 -0
- fluxplot/stats/_common.py +196 -0
- fluxplot/stats/multi_group.py +443 -0
- fluxplot/stats/paired.py +209 -0
- fluxplot/stats/two_group.py +149 -0
- fluxplot/style.py +469 -0
- fluxplot/surface.py +487 -0
- fluxplot/surface3d.py +197 -0
- fluxplot/tagger.py +561 -0
- fluxplot/version.py +19 -0
- fluxplot-0.1.0.dist-info/METADATA +1199 -0
- fluxplot-0.1.0.dist-info/RECORD +65 -0
- fluxplot-0.1.0.dist-info/WHEEL +4 -0
- fluxplot-0.1.0.dist-info/licenses/LICENSE +21 -0
- fluxplot-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +472 -0
fluxplot/__init__.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""FluxPlot — matplotlib, but every meaningful thing has a name.
|
|
2
|
+
|
|
3
|
+
A thin, additive semantic-tagging layer over matplotlib. Plot in real matplotlib (using the
|
|
4
|
+
``fp.*`` convenience helpers or by tagging raw artists), then :func:`save` emits a semantic SVG +
|
|
5
|
+
``*.fluxplot.json`` manifest + ``*.recipe.json``.
|
|
6
|
+
|
|
7
|
+
See ``Flux_SemanticSVG_Spec.md`` for the conceptual spec.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from . import (
|
|
11
|
+
colorcheck, # noqa: E402,F401 (accessibility lint — colorcheck.check_palette, simulate, contrast, …)
|
|
12
|
+
colors, # noqa: E402,F401 (canonical palette/colormaps — colors.green400, colors.maps.emerald, …)
|
|
13
|
+
colorscale, # noqa: E402,F401 (the portable colour-scale law — colorscale.apply(record, values))
|
|
14
|
+
stats, # noqa: E402,F401 (tests behind the plots — stats.welch_hedges(a, b), …)
|
|
15
|
+
style, # noqa: E402,F401 (house plotting style — fx.use_light(), fx.FLEXOKI, …)
|
|
16
|
+
)
|
|
17
|
+
from .api import ( # noqa: E402,F401
|
|
18
|
+
annotation,
|
|
19
|
+
area,
|
|
20
|
+
band,
|
|
21
|
+
bar,
|
|
22
|
+
barh,
|
|
23
|
+
box,
|
|
24
|
+
errorbar,
|
|
25
|
+
hist,
|
|
26
|
+
legend,
|
|
27
|
+
line,
|
|
28
|
+
reference_line,
|
|
29
|
+
save,
|
|
30
|
+
scatter,
|
|
31
|
+
secondary_axis,
|
|
32
|
+
significance_bracket,
|
|
33
|
+
stem,
|
|
34
|
+
step,
|
|
35
|
+
tag,
|
|
36
|
+
tag_points,
|
|
37
|
+
tag_seaborn,
|
|
38
|
+
violin,
|
|
39
|
+
)
|
|
40
|
+
from .recipe import (
|
|
41
|
+
params, # noqa: E402,F401 (overridable tunables for rerun-plot/Regenerate)
|
|
42
|
+
)
|
|
43
|
+
from .surface import ( # noqa: E402,F401
|
|
44
|
+
surface,
|
|
45
|
+
)
|
|
46
|
+
from .signature_fluxplots import ( # noqa: E402,F401 (preset plot types unique to Flux)
|
|
47
|
+
fluxbox,
|
|
48
|
+
glowbar,
|
|
49
|
+
hexmatrix,
|
|
50
|
+
)
|
|
51
|
+
from .style import ( # noqa: E402,F401 (re-exported for convenience)
|
|
52
|
+
use_dark,
|
|
53
|
+
use_light,
|
|
54
|
+
use_paper,
|
|
55
|
+
)
|
|
56
|
+
from .scene3d import Scene3D, scene3d, SCENE3D_SPEC_VERSION
|
|
57
|
+
from .mesh3d import mesh3d, can_morph
|
|
58
|
+
from .surface3d import surface3d
|
|
59
|
+
from .fields import heatmap, contour, contourf, colorbar, color_scale
|
|
60
|
+
from .brackets import brackets
|
|
61
|
+
from .images import image, scalebar
|
|
62
|
+
from .fits import regression, kde
|
|
63
|
+
from .panels import panel
|
|
64
|
+
from .version import SPEC_VERSION, __version__ # noqa: F401
|
|
65
|
+
|
|
66
|
+
__all__ = [
|
|
67
|
+
"__version__",
|
|
68
|
+
"Scene3D", "scene3d", "mesh3d", "surface3d", "can_morph", "SCENE3D_SPEC_VERSION",
|
|
69
|
+
"surface",
|
|
70
|
+
"glowbar",
|
|
71
|
+
"fluxbox",
|
|
72
|
+
"hexmatrix",
|
|
73
|
+
"panel",
|
|
74
|
+
"heatmap",
|
|
75
|
+
"contour",
|
|
76
|
+
"contourf",
|
|
77
|
+
"colorbar",
|
|
78
|
+
"color_scale",
|
|
79
|
+
"brackets",
|
|
80
|
+
"image",
|
|
81
|
+
"scalebar",
|
|
82
|
+
"regression",
|
|
83
|
+
"kde",
|
|
84
|
+
"SPEC_VERSION",
|
|
85
|
+
"colors",
|
|
86
|
+
"colorcheck",
|
|
87
|
+
"colorscale",
|
|
88
|
+
"stats",
|
|
89
|
+
"style",
|
|
90
|
+
"use_light",
|
|
91
|
+
"use_dark",
|
|
92
|
+
"use_paper",
|
|
93
|
+
"params",
|
|
94
|
+
"legend",
|
|
95
|
+
"line",
|
|
96
|
+
"scatter",
|
|
97
|
+
"secondary_axis",
|
|
98
|
+
"step",
|
|
99
|
+
"stem",
|
|
100
|
+
"bar",
|
|
101
|
+
"barh",
|
|
102
|
+
"errorbar",
|
|
103
|
+
"area",
|
|
104
|
+
"band",
|
|
105
|
+
"box",
|
|
106
|
+
"violin",
|
|
107
|
+
"hist",
|
|
108
|
+
"tag",
|
|
109
|
+
"tag_points",
|
|
110
|
+
"tag_seaborn",
|
|
111
|
+
"significance_bracket",
|
|
112
|
+
"reference_line",
|
|
113
|
+
"annotation",
|
|
114
|
+
"save",
|
|
115
|
+
]
|
fluxplot/_fieldmap.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""One value-to-color law shared by 2D surface and Scene3D; no geometry or rendering."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import numpy as np
|
|
4
|
+
from matplotlib.colors import Normalize, to_hex, to_rgba
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _normalise_missing(values, missing_below=None, missing_values=()):
|
|
8
|
+
"""Normalize explicit sentinel codes; zero remains a real observation."""
|
|
9
|
+
v=np.asarray(values,dtype=float).copy()
|
|
10
|
+
if missing_below is not None: v[v<missing_below]=np.nan
|
|
11
|
+
for mv in missing_values: v[v==mv]=np.nan
|
|
12
|
+
return v
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _resolve_range(finite,color_range,percentile):
|
|
16
|
+
if color_range is not None: return float(color_range[0]),float(color_range[1])
|
|
17
|
+
if percentile is not None: return float(np.percentile(finite,percentile[0])),float(np.percentile(finite,percentile[1]))
|
|
18
|
+
return float(np.min(finite)),float(np.max(finite))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def category_name(code, categories=None):
|
|
22
|
+
"""Name of one integer label code: ``categories[code]``, else ``category-<code>``.
|
|
23
|
+
|
|
24
|
+
Negative codes are spelled ``category-m<abs>`` (``-1`` -> ``category-m1``). A plain
|
|
25
|
+
``category--1`` would slugify to ``category-1`` and collide with code ``+1``, so the
|
|
26
|
+
id of one category would shift depending on whether the other is present.
|
|
27
|
+
"""
|
|
28
|
+
code = int(code)
|
|
29
|
+
if categories and code in categories:
|
|
30
|
+
return categories[code]
|
|
31
|
+
return f'category-{code}' if code >= 0 else f'category-m{-code}'
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def categorical_colors(names,palette=None,categories=None):
|
|
35
|
+
"""Resolve the fixed category-name/code palette in stable sorted-name order.
|
|
36
|
+
|
|
37
|
+
A category the palette does not name takes its colour from the registry
|
|
38
|
+
(``fp.colors.categories``): pinned, or the next free slot of the active cycle, remembered —
|
|
39
|
+
so ``'b'`` is the same colour whether or not ``'a'`` is present, in every figure.
|
|
40
|
+
"""
|
|
41
|
+
from .colors import categories as _registry
|
|
42
|
+
resolved={}
|
|
43
|
+
for name in sorted(names):
|
|
44
|
+
color=None
|
|
45
|
+
if palette:
|
|
46
|
+
color=palette.get(name)
|
|
47
|
+
if color is None and categories:
|
|
48
|
+
for code,nm in categories.items():
|
|
49
|
+
if nm==name and code in palette:
|
|
50
|
+
color=palette[code]; break
|
|
51
|
+
if color is None: color=_registry.get(name)
|
|
52
|
+
rgba=to_rgba(color)
|
|
53
|
+
resolved[name]=to_hex(rgba, keep_alpha=rgba[3] < 1) # an opaque colour stays #rrggbb
|
|
54
|
+
return resolved
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def continuous_mapping(finite,cmap=None,color_range=None,percentile=None):
|
|
58
|
+
"""Return the shared matplotlib colormap and Normalize, including all-missing fallback."""
|
|
59
|
+
if finite.size==0 and color_range is None: lo,hi=0.,1.
|
|
60
|
+
else: lo,hi=_resolve_range(finite,color_range,percentile)
|
|
61
|
+
if not np.isfinite([lo,hi]).all() or lo>hi: raise ValueError('color_range must be finite and nondecreasing')
|
|
62
|
+
return resolve_colormap(cmap),Normalize(vmin=lo,vmax=hi)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def resolve_colormap(cmap=None):
|
|
66
|
+
"""A Colormap from ``None`` (the style default), a Colormap, or a name.
|
|
67
|
+
|
|
68
|
+
Names resolve through matplotlib first (so existing names keep their exact maps),
|
|
69
|
+
then through fluxplot's shipped collections: ``'emerald'``, ``'crameri.batlow'``,
|
|
70
|
+
``'batlow_r'``, or a map added with ``fp.colors.maps.register``.
|
|
71
|
+
"""
|
|
72
|
+
import matplotlib as mpl
|
|
73
|
+
if cmap is None:
|
|
74
|
+
return mpl.colormaps[mpl.rcParams['image.cmap']]
|
|
75
|
+
if not isinstance(cmap, str):
|
|
76
|
+
return cmap
|
|
77
|
+
try:
|
|
78
|
+
return mpl.colormaps[cmap]
|
|
79
|
+
except KeyError:
|
|
80
|
+
pass
|
|
81
|
+
from .colors import maps
|
|
82
|
+
try:
|
|
83
|
+
return maps.get(cmap)
|
|
84
|
+
except KeyError:
|
|
85
|
+
raise ValueError(f"unknown colormap {cmap!r}: use a matplotlib name ('viridis') or a "
|
|
86
|
+
"fluxplot map ('emerald', 'crameri.batlow'); list them with "
|
|
87
|
+
"fp.colors.maps.collections()") from None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def infer_kind(finite,kind):
|
|
91
|
+
if kind=='categorical': kind='label'
|
|
92
|
+
if kind=='auto':
|
|
93
|
+
if not finite.size: raise ValueError("every vertex is missing and kind='auto' cannot infer label vs continuous; pass kind explicitly")
|
|
94
|
+
uniq=np.unique(finite)
|
|
95
|
+
kind='label' if uniq.size<=32 and np.allclose(uniq,np.round(uniq)) else 'continuous'
|
|
96
|
+
if kind not in ('label','continuous'): raise ValueError("kind must be 'label', 'continuous', or 'auto'")
|
|
97
|
+
return kind
|
fluxplot/_mesh_reduce.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Optional collapse replay; named-part boundaries never share a simplification pass."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import warnings
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def face_budgets(counts, total):
|
|
8
|
+
"""Apportion one triangle per part, then remaining capacity by largest remainder."""
|
|
9
|
+
counts=[int(n) for n in counts]
|
|
10
|
+
if total is None: return [None]*len(counts)
|
|
11
|
+
if total<len(counts): raise ValueError('max_faces must allow at least one triangle per part')
|
|
12
|
+
if any(n<1 for n in counts): raise ValueError('mesh parts must contain triangles')
|
|
13
|
+
target=min(int(total),sum(counts)); remaining=target-len(counts)
|
|
14
|
+
capacity=sum(n-1 for n in counts)
|
|
15
|
+
if not capacity: return counts
|
|
16
|
+
portions=[divmod(remaining*(n-1),capacity) for n in counts]
|
|
17
|
+
result=[1+q for q,r in portions]
|
|
18
|
+
for i in sorted(range(len(counts)),key=lambda i:(-portions[i][1],i))[:target-sum(result)]: result[i]+=1
|
|
19
|
+
return result
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def reduce_part(part, *, max_faces=None, collapses=None):
|
|
23
|
+
try: import fast_simplification as fs
|
|
24
|
+
except ImportError as exc:
|
|
25
|
+
raise ImportError('max_faces needs the optional fluxplot[mesh] extra (fast-simplification); omit max_faces to save full resolution') from exc
|
|
26
|
+
vertices=part.vertices.copy(); faces=part.faces.copy()
|
|
27
|
+
if collapses is None:
|
|
28
|
+
_,_,collapses=fs.simplify(vertices,faces,target_count=int(max_faces),return_collapses=True)
|
|
29
|
+
collapses=np.asarray(collapses,dtype=np.int32)
|
|
30
|
+
out, outfaces, mapping=fs.replay_simplification(vertices.astype(np.float32),faces,collapses)
|
|
31
|
+
mapping=np.asarray(mapping,dtype=np.int64)
|
|
32
|
+
if len(outfaces)==0: raise ValueError(f'{part.id}: max_faces removed every triangle; choose a larger budget')
|
|
33
|
+
if max_faces is not None and len(outfaces)>max_faces:
|
|
34
|
+
warnings.warn(f'{part.id}: simplifier retained {len(outfaces)} faces above requested max_faces={max_faces}',stacklevel=3)
|
|
35
|
+
# Some backends leave unused output vertices. Compact by index only, in the
|
|
36
|
+
# same order for all states and fields; this bounds stored vertices by 3F.
|
|
37
|
+
used=np.unique(outfaces)
|
|
38
|
+
for name,target in list(part.states.items()):
|
|
39
|
+
sv,sf,_=fs.replay_simplification(target.astype(np.float32),faces,collapses)
|
|
40
|
+
if not np.array_equal(outfaces,sf): raise ValueError(f'state {name}: collapse replay changed topology')
|
|
41
|
+
part.states[name]=np.asarray(sv,dtype=float)[used]
|
|
42
|
+
def remap(array):
|
|
43
|
+
a=np.asarray(array,dtype=float); scalar=a.ndim==1
|
|
44
|
+
if scalar: a=a[:,None]
|
|
45
|
+
valid=np.isfinite(a); good=(mapping>=0)&(mapping<len(out))
|
|
46
|
+
sums=np.zeros((len(out),a.shape[1])); count=np.zeros_like(sums)
|
|
47
|
+
np.add.at(sums,mapping[good],np.where(valid[good],a[good],0))
|
|
48
|
+
np.add.at(count,mapping[good],valid[good].astype(float))
|
|
49
|
+
result=np.divide(sums,count,out=np.full_like(sums,np.nan),where=count>0)
|
|
50
|
+
return result[:,0] if scalar else result
|
|
51
|
+
if part.values is not None: part.values=remap(part.values)[used]
|
|
52
|
+
if part.colors is not None: part.colors=remap(part.colors)[used]
|
|
53
|
+
part.vertices=np.asarray(out,dtype=float)[used]; part.faces=np.searchsorted(used,outfaces).astype(np.int64)
|
|
54
|
+
part.collapses=collapses.copy()
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Conservative, scene-specific face caps without running a simplifier."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
import json
|
|
6
|
+
import struct
|
|
7
|
+
|
|
8
|
+
WARN_BYTES = 50 * 1024**2
|
|
9
|
+
WARN_TRIANGLES = 2_000_000
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _json_bytes(value, *, upper):
|
|
13
|
+
# Counts/offsets are bounded by GLB's uint32 size; finite float32 bounds and
|
|
14
|
+
# Python float weights need at most 25 characters. Reserve 34, including
|
|
15
|
+
# quotes, so simple original bounds cannot underestimate collapsed bounds.
|
|
16
|
+
def numbers(item):
|
|
17
|
+
if isinstance(item, dict): return {k: numbers(v) for k, v in item.items()}
|
|
18
|
+
if isinstance(item, list): return [numbers(v) for v in item]
|
|
19
|
+
if isinstance(item, (int, float)) and not isinstance(item, bool):
|
|
20
|
+
return '0' * 32 if upper else 0
|
|
21
|
+
return item
|
|
22
|
+
size = len(json.dumps(numbers(value), sort_keys=True, separators=(',', ':'), allow_nan=False).encode())
|
|
23
|
+
return (size + 3) // 4 * 4
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def face_cap_recommendation(scene, glb, *, max_bytes=None, max_triangles=None):
|
|
27
|
+
"""Return an actionable per-helper-call cap, or explain why none is safe.
|
|
28
|
+
|
|
29
|
+
Each part is bounded independently by min(original faces, cap), even when
|
|
30
|
+
a call apportions its cap among several parts. The byte bound uses at most
|
|
31
|
+
three referenced vertices per face, every attribute/state, uint32 indices,
|
|
32
|
+
and the actual JSON structure/strings with worst-case numeric widths.
|
|
33
|
+
This is a sufficient bound, not a prediction of backend simplification.
|
|
34
|
+
"""
|
|
35
|
+
max_bytes = WARN_BYTES if max_bytes is None else max_bytes
|
|
36
|
+
max_triangles = WARN_TRIANGLES if max_triangles is None else max_triangles
|
|
37
|
+
# A scene may already have been simplified. The hint applies to rerunning
|
|
38
|
+
# the authored calls, so use their original face counts, not a prior quota.
|
|
39
|
+
counts = [len(part.source_faces) if part.source_faces is not None else len(part.faces)
|
|
40
|
+
for part in scene.parts]
|
|
41
|
+
total = sum(counts)
|
|
42
|
+
limit = f'{max_bytes / 1024**2:g} MiB / {max_triangles:,} triangles'
|
|
43
|
+
if len(counts) > max_triangles:
|
|
44
|
+
return f'No max_faces cap can fit {limit} while preserving all {len(counts):,} parts; split the scene or remove parts'
|
|
45
|
+
if total == len(counts) and len(glb) > max_bytes:
|
|
46
|
+
return f'No max_faces cap can fit {limit}: all {len(counts):,} parts already contain one triangle; use fewer parts or shape states, or split the scene'
|
|
47
|
+
|
|
48
|
+
size = struct.unpack_from('<I', glb, 12)[0]
|
|
49
|
+
document = json.loads(glb[20:20 + size])
|
|
50
|
+
# Even ignoring every buffer and shortening every number, these names,
|
|
51
|
+
# target lists and structural keys cannot be removed by a face cap.
|
|
52
|
+
required = deepcopy(document)
|
|
53
|
+
optional_accessors, optional_views = set(), set()
|
|
54
|
+
for mesh in required['meshes']:
|
|
55
|
+
for primitive in mesh['primitives']:
|
|
56
|
+
index = primitive['attributes'].pop('_VALID', None)
|
|
57
|
+
if index is not None:
|
|
58
|
+
optional_accessors.add(index)
|
|
59
|
+
optional_views.add(required['accessors'][index]['bufferView'])
|
|
60
|
+
required['accessors'] = [a for i, a in enumerate(required['accessors']) if i not in optional_accessors]
|
|
61
|
+
required['bufferViews'] = [v for i, v in enumerate(required['bufferViews']) if i not in optional_views]
|
|
62
|
+
for material in required['materials']: material.pop('alphaMode', None)
|
|
63
|
+
if 28 + _json_bytes(required, upper=False) > max_bytes:
|
|
64
|
+
return f'No max_faces cap can fit {limit}: fixed GLB part/state metadata alone exceeds the byte limit; use fewer parts or shape states, or split the scene'
|
|
65
|
+
|
|
66
|
+
# Averaging valid values cannot add a channel normally, but reserve _VALID
|
|
67
|
+
# even for presently all-valid data so the bound does not rely on that.
|
|
68
|
+
for part, mesh in zip(scene.parts, document['meshes']):
|
|
69
|
+
attrs = mesh['primitives'][0]['attributes']
|
|
70
|
+
if part.values is not None and '_VALID' not in attrs:
|
|
71
|
+
attrs['_VALID'] = len(document['accessors'])
|
|
72
|
+
document['accessors'].append({'bufferView': len(document['bufferViews']), 'componentType': 5121, 'count': 1, 'type': 'SCALAR'})
|
|
73
|
+
document['bufferViews'].append({'buffer': 0, 'byteOffset': 0, 'byteLength': 4, 'target': 34962, 'byteStride': 4})
|
|
74
|
+
for material in document['materials']: material['alphaMode'] = 'BLEND'
|
|
75
|
+
overhead = 28 + _json_bytes(document, upper=True)
|
|
76
|
+
costs = [24 + 24 * len(p.states) + (8 if p.values is not None else 0) + (4 if p.colors is not None else 0) for p in scene.parts]
|
|
77
|
+
|
|
78
|
+
def fits(cap):
|
|
79
|
+
faces = [min(count, cap) for count in counts]
|
|
80
|
+
# Compaction after reduction guarantees at most three vertices per face.
|
|
81
|
+
# Do not cap this by today's vertex count: a user may rerun an already
|
|
82
|
+
# simplified scene from its original meshes with the recommended cap.
|
|
83
|
+
binary = sum(3 * count * cost + 12 * count for count, cost in zip(faces, costs))
|
|
84
|
+
return sum(faces) <= max_triangles and overhead + binary <= max_bytes
|
|
85
|
+
|
|
86
|
+
low = max(1, scene._max_call_parts)
|
|
87
|
+
if not fits(low):
|
|
88
|
+
return f'No conservative max_faces cap can be recommended for {limit} while preserving every call\'s parts and shape states; use fewer parts or states, or split the scene'
|
|
89
|
+
high = max(counts)
|
|
90
|
+
while low < high:
|
|
91
|
+
middle = (low + high + 1) // 2
|
|
92
|
+
if fits(middle): low = middle
|
|
93
|
+
else: high = middle - 1
|
|
94
|
+
return (f'use max_faces={low} on each mesh3d/surface3d call to fit {limit} '
|
|
95
|
+
'(conservative bound if the simplifier reaches each requested face cap; retained-face warnings still apply)')
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
three 0.186.1
|
|
2
|
+
|
|
3
|
+
The MIT License
|
|
4
|
+
|
|
5
|
+
Copyright © 2010-2026 three.js authors
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in
|
|
15
|
+
all copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
23
|
+
THE SOFTWARE.
|