feat: opstool-style scene recipe for the Plotly canvas
Frames the view deterministically: Scene now carries padded per-axis bounds (opstool's pad_ratio 0.15, with unit slack on degenerate axes) plus axis_overrides() pinning range + autorange=False, so the model is never flush against the viewport edge. Adds entity hover templates (Node #n / Element #n with an empty extra tag), and an optional axis outline (grid + ticks) - off by default so the SAP2000-like clean viewport is unchanged; the coloured X/Y/Z axis lines always stay as the orientation cue. The outline is exposed as a bool in the Plot Properties table, so the table-driven dialog now builds a checkbox for kind=bool.
This commit is contained in:
parent
bd365be77d
commit
5f977e9df0
5 changed files with 128 additions and 7 deletions
|
|
@ -206,3 +206,57 @@ def test_frame_trace_meta_marks_elements_pickable() -> None:
|
|||
# None separators break the line into per-element segments.
|
||||
assert None in frames["x"]
|
||||
assert len(frames["customdata"]) == len(frames["x"])
|
||||
|
||||
|
||||
# ── scene recipe (opstool-derived framing) ───────────────────────────────
|
||||
def test_axis_bounds_are_padded_around_the_model() -> None:
|
||||
project = _load("cantilever")
|
||||
scene = PlotlyTraceBuilder().build(project, SceneOptions())
|
||||
points = np.array([node.coords for node in project.nodes], dtype=float)
|
||||
for index, axis in enumerate(("x", "y", "z")):
|
||||
low, high = scene.axis_bounds[axis]
|
||||
assert low < points[:, index].min() or low <= points[:, index].min()
|
||||
assert high > points[:, index].max() or high >= points[:, index].max()
|
||||
assert scene.diagonal > 0
|
||||
|
||||
|
||||
def test_planar_model_gets_unit_slack_on_the_flat_axis() -> None:
|
||||
project = _load("basic_truss")
|
||||
scene = PlotlyTraceBuilder().build(project, SceneOptions())
|
||||
low, high = scene.axis_bounds["z"]
|
||||
assert high - low > 0 # a degenerate axis must not collapse the view
|
||||
|
||||
|
||||
def test_axis_overrides_pin_ranges_with_autorange_off() -> None:
|
||||
scene = PlotlyTraceBuilder().build(_load("cantilever"), SceneOptions())
|
||||
overrides = scene.axis_overrides()
|
||||
assert set(overrides) == {"xaxis", "yaxis", "zaxis"}
|
||||
for axis, override in overrides.items():
|
||||
assert override["autorange"] is False
|
||||
assert len(override["range"]) == 2
|
||||
assert override["range"] == list(scene.axis_bounds[axis[0]])
|
||||
|
||||
|
||||
def test_hover_templates_identify_entities() -> None:
|
||||
scene = PlotlyTraceBuilder().build(_load("basic_truss"), SceneOptions())
|
||||
(nodes,) = _traces(scene, "nodes")
|
||||
(frames,) = _traces(scene, "elements")
|
||||
assert nodes["hovertemplate"] == "Node #%{customdata}<extra></extra>"
|
||||
assert frames["hovertemplate"] == "Element #%{customdata}<extra></extra>"
|
||||
# Hover must not fall back to the raw-id "skip" mode.
|
||||
assert "hoverinfo" not in nodes
|
||||
|
||||
|
||||
def test_axis_outline_flag_toggles_grid_and_ticks() -> None:
|
||||
plain = RenderStyle()
|
||||
outlined = RenderStyle(show_axis_outline=True)
|
||||
project = _load("cantilever")
|
||||
scene_plain = PlotlyTraceBuilder(plain).build(project, SceneOptions())
|
||||
scene_outlined = PlotlyTraceBuilder(outlined).build(project, SceneOptions())
|
||||
|
||||
off_axis = scene_plain.layout["scene"]["xaxis"]
|
||||
on_axis = scene_outlined.layout["scene"]["xaxis"]
|
||||
assert off_axis["showgrid"] is False and off_axis["showticklabels"] is False
|
||||
assert on_axis["showgrid"] is True and on_axis["showticklabels"] is True
|
||||
# The coloured axis lines stay visible either way (orientation cue).
|
||||
assert off_axis["visible"] is True and on_axis["visible"] is True
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ def test_editable_subset_matches_the_field_table() -> None:
|
|||
kinds = {name: kind for name, _label, kind in STYLE_FIELDS}
|
||||
for name in EDITABLE_FIELDS:
|
||||
assert hasattr(style, name), name
|
||||
assert kinds[name] in {"color", "float", "int"}
|
||||
assert kinds[name] in {"color", "float", "int", "bool"}
|
||||
|
||||
|
||||
def test_response_colorscale_is_an_evenly_spaced_mapping() -> None:
|
||||
|
|
|
|||
Loading…
Reference in a new issue