Skip to main content

ANYtk3D

Fast NumPy-backed 3D drawing on a Tkinter Canvas.

ANYtk3D is the standalone 3D viewport extracted from ANYstructure. It renders solids, stiffened plates, cylinders and arbitrary meshes directly on a tkinter.Canvas — no OpenGL, matplotlib or second GUI event loop. Shared geometry, retained-array and selection contracts come from ANY3dView.

Features

  • Typical shapes out of the box — box, sphere, cone, truncated cone, cylinder, tube/pipe, torus, pyramid, wedge, prism, swept extrusion, disk/annulus, plane, arrow, ground grid, structural beam profiles (FB, T, I, L, C, BOX) and arbitrary index meshes
  • Directional lighting — ambient + Lambert diffuse + Blinn-Phong highlight, flat-shaded per face, two-sided, world-fixed or head-lamp
  • Correct depth order — every face is painted back to front by camera-space depth, closed solids cull their back faces, faces straddling the near plane are clipped instead of dropped, and 3D lines take part in the same sort
  • Layered transparency — 16 screen-door density steps with non-overlapping windows per layer, so a transparent shell shows its far wall, its contents and its near wall at once
  • Built for speed — geometry is compiled to numpy arrays once and every frame is vectorised; canvas items are pooled and only reconfigured when their appearance changes; the interactive level of detail self-tunes from measured frame times
  • Result fields and animation — a batched add_faces call for meshes coloured per element, plus frame capture and playback that adapts its detail to the requested frame rate
  • Retained FE arrays — zero-copy MeshArrays, packed semantic owners, displacement and scalar updates, active/selection masks, transforms and local chunk replacement through stable MeshHandle objects
  • Camera orbit, zoom and pan; plate colour-coding by thickness (or any scalar) with a fixed legend; X/Y/Z axis overlay and optional rulers; animation caching and playback

Installation

pip install ANYtk3D

Tkinter ships with the standard CPython installers.

Quick start

import tkinter as tk
from anytk3d import Point3D, Tkinter3DCanvas

root = tk.Tk()
canvas = Tkinter3DCanvas(root, width=900, height=600, bg='white')
canvas.pack(fill='both', expand=True)

canvas.add_box(2.0, 1.0, 0.5, center=Point3D(-3, 0, 0.25), color='#4e79a7')
canvas.add_sphere(0.8, center=Point3D(0, 0, 0.8), color='#f28e2b')
canvas.add_tube(0.8, 0.5, 2.0, center=Point3D(3, 0, 1.0), color='#59a14f')
canvas.add_beam(Point3D(-4, 3, 0), Point3D(4, 3, 0), kind='I',
                web_height=0.6, flange_width=0.3)
canvas.add_grid(size_x=12, size_y=10)

canvas.fit_to_scene()
root.mainloop()

Retained arrays

import numpy as np
from anytk3d import MeshArrays

handle = canvas.add_mesh_arrays(MeshArrays(
    positions=np.asarray(nodes, np.float32),
    triangles=np.asarray(connectivity, np.uint32),
    triangle_to_element=np.asarray(triangle_to_element, np.uint32),
    element_ids=np.asarray(element_ids, np.uint64),
    element_scalars=np.asarray(stress, np.float32),
))

handle.update_displacements(next_displacements)
handle.set_deformation_scale(20.0)
handle.set_active_elements(active_mask)
handle.set_selected_elements(selected_elements)

# Incremental chunks may carry their own packed semantic owner table.
handle.add_chunk("local-remesh", replacement_arrays, owners=chunk_owners)

Compatible C-contiguous arrays are retained without copying and are treated as immutable while registered. Scalar-only changes reuse connectivity, geometry and packed ownership. Selected elements receive the same application-selection tint as the GPU backend. Chunk-local owner tables drive face, line, and point picking; when omitted, the handle's legacy tag remains the compatibility fallback. Cross-thread producers can use canvas.submit_update(handle.update_element_scalars, immutable_values).

Shared backend contract

ANYtk3D 0.5 implements the renderer-neutral integration contract shared with the optional ANY3dView GPU viewer. Applications can use any3dview.create_viewer and select backend="auto", "gpu", or "software" without importing a concrete renderer up front. The software viewer reports backend_name == "software"; backend_diagnostics retains any reason that automatic GPU selection fell back.

Use event_widget for input bindings and viewport_size for the current drawable size. project_point, project_points, screen_ray, and unproject_to_plane form the public modelling/projection boundary and avoid access to Tk-specific canvas internals.

from any3dview import create_viewer

viewer = create_viewer(root, backend="auto")
viewer.event_widget.bind("<Escape>", cancel_current_tool)
screen_position = viewer.project_point(world_position)

Live renderer replacement transfers camera and view policy with export_view_state() and apply_view_state(). Geometry is deliberately not part of ViewerState: populate the candidate viewer from application-owned scene data, apply the state, and only then replace the working widget. Pick, ViewerState, ViewerBackend, and the established geometry and selection types are exact re-exports of their ANY3dView definitions.

ANYtk3D 0.5.2 also re-exports SemanticRef, VisibilityState, ViewerCommand, ViewerCommandController, and viewer_command_manifest with exact class identity. Semantic hide/show/isolate policies filter retained faces, lines, points and legacy bound primitives without deleting scene data, and hidden primitives are excluded from projection-based selection.

capture_image() returns an RGBA Pillow image of the mapped inner canvas when Pillow is installed (the integrated GUI application extras install it). The base ANYtk3D import remains Pillow-independent; an unmapped or unavailable desktop reports an actionable RuntimeError instead of capturing window chrome or an unrelated screen region.

Shapes

add_box, add_box_from_bounds, add_sphere, add_cone, add_frustum, add_cylinder, add_tube, add_torus, add_pyramid, add_wedge, add_prism, add_extrusion, add_disk, add_plane, add_arrow, add_beam, add_grid, add_mesh, add_shape, plus the original add_polygon, add_line, add_text, add_rectangular_plate, add_flat_stiffener, add_flat_girder, add_longitudinal_stiffener and add_ring_stiffener.

Every shape builder takes center/axis placement and the same material keywords — color, outline, opacity, layer, cull_backface, lit, back_color, face_colors, tags.

The tessellation lives in anytk3d.shapes as plain functions returning a Mesh (vertices plus index faces), so it can be used, combined and tested without a display:

from anytk3d import shapes

mesh = shapes.torus(2.0, 0.4).merged(shapes.sphere(0.5))
canvas.add_shape(mesh, position=Point3D(0, 0, 3), color='#b07aa1')

Lighting

canvas.set_light(direction=Point3D(0.4, -0.6, 0.7), ambient=0.45,
                 diffuse=0.55, specular=0.12)
canvas.set_light(follow_camera=True)   # head lamp that orbits with the view
canvas.set_shading(False)              # flat colours, no shading

Ambient and diffuse sum to 1.0 by default, so a face turned toward the light renders in its exact base colour and every other face only darkens. That keeps colour-coded scenes matching their legend.

Transparency

opacity runs from 0 to 1 in 16 usable steps. A Tk canvas has no alpha channel, so this is screen-door stippling — but the front and back of a surface are given non-overlapping dither windows, sized so that stacking them reproduces ordinary alpha compositing (1 - (1 - a)^2). Passing an explicit stipple= string still uses that exact Tk pattern.

canvas.add_sphere(2.0, color='#7fb3d5', opacity=0.35)   # see the contents
canvas.add_box(1.0, 1.0, 1.0, color='#c0392b')          # ...through the shell

Picking and highlighting

Every shape builder takes tags, and those tags reach the underlying Tk canvas item. Picking uses Tk's own hit testing, so the topmost item under the cursor is the nearest one — no ray casting, and occlusion is correct for free.

canvas.add_box(2, 1, 0.5, tags='plate7')

def on_pick(pick):
    print(pick.tag, pick.shift)          # '' when the click missed everything
    canvas.set_highlight([pick.tag] if pick.tag else [])

canvas.set_pick_callback(on_pick, prefix='plate')   # prefix is optional
canvas.set_hover_callback(lambda pick: ...)         # fires only on change
canvas.pick_at(x, y)                                # query without an event

A click is a press and release without a drag, so picking coexists with pan and orbit. Highlighting is applied while rendering rather than by reconfiguring Tk items, so it survives the next redraw; the resolution from tags to faces is cached per scene and highlight generation.

Picking is opt-in — with no callback set, the canvas behaves exactly as before.

For CAD/FE-style interaction, opt into the commercial profile. LMB selects and draws a directional box, MMB pans, RMB orbits, and the wheel zooms. A left-to-right box requires full containment; right-to-left is crossing.

from anytk3d import (
    PickBinding, SelectionConfig, SelectionDepth, SelectionFilter,
)

canvas = Tkinter3DCanvas(root, interaction_profile="commercial")
canvas.add_faces(
    element_polygons,
    bindings=[
        PickBinding.one(f"element{number}", "mesh.element")
        for number in element_numbers
    ],
)
canvas.configure_selection(
    lambda event: print(event.operation, [hit.key for hit in event.hits]),
    hover_callback=lambda hit: print(None if hit is None else hit.key),
    config=SelectionConfig(
        filter=SelectionFilter(kinds=frozenset({"mesh.element"})),
        depth=SelectionDepth.VISIBLE,
    ),
)

No modifier replaces the selection, Shift adds, Ctrl toggles, and Alt removes. query_point, query_rectangle, screen_ray, and unproject_to_plane expose the same projected geometry for modelling tools. The default interaction profile remains legacy for compatibility.

Section planes

World-space section planes clip faces, lines, markers, text and projected selection geometry without changing the retained scene. The normal is normalized and the half-space normal · point >= offset is retained; the cut surface is intentionally left open rather than capped.

canvas.set_section_plane(normal=(1, 0, 0), offset=1.5)
canvas.clear_section_plane()

The plane remains active during orbit, animation and fast interactive frames.

Colour scale

Plate colour-coding, the legend and thickness_color all share one scale. Replace it to use a different colour map — sample any ramp at a handful of positions and hand the stops over:

from matplotlib import colormaps, colors   # only if you want matplotlib maps
import anytk3d

stops = [(t / 16, colors.to_hex(colormaps['viridis'](t / 16))) for t in range(17)]
anytk3d.set_color_stops(stops)
anytk3d.get_color_stops()
anytk3d.reset_color_stops()                # back to the built-in blue→red ramp

Colours a canvas resolved itself (cylinder plate thickness, stiffener thickness) refresh on the next redraw. anytk3d.DEFAULT_COLOR_STOPS is the built-in scale; assigning to the old _THICKNESS_COLOR_STOPS constant has no effect — use set_color_stops.

Result fields

add_faces is the batched path for a mesh where every element carries its own colour — an FE stress plot, a deformed shape, a utilisation map. It takes the whole element set in one call and computes all the centroids and normals as array operations:

canvas.add_faces(polygons, colors=element_colors, outline='#64748b')
canvas.set_thickness_legend([0, 80, 160, 240], unit='MPa', title='von Mises')

polygons is a sequence of vertex sequences (Point3D or (x, y, z)), or an (faces, vertices, 3) array. On a 4900-element field this compiles in 4.5 ms against 48 ms for the same elements added one at a time.

Animation

Capture a scene per step, then replay it:

canvas.begin_animation_cache()
for step in range(frames):
    canvas.clear(keep_canvas=True)
    canvas.add_faces(deformed_shape(step), colors=field_colors(step))
    canvas.capture_animation_frame()
canvas.play_animation(fps=30)          # fast=None/True/False

A scene without cylinders or stiffeners compiles to a single shared representation, so each captured frame costs one build rather than two. Playback defaults to fast=None, which starts at full detail and drops to the reduced-detail path as soon as a frame overruns its slot; pass True or False to pin it. animation_frames, animation_frame_index and is_playing_animation are available for a progress readout.

Performance

Interactive frames use a reduced-detail scene with a face budget that adapts to the measured frame rate, so orbiting stays responsive on dense models and full detail returns when the mouse is released.

canvas.set_interactive_detail(4000)   # starting face budget while dragging
canvas.set_mesh_lines(False)          # drop per-face outlines
canvas.set_occlude_lines(False)       # keep 3D lines on top of geometry

Demos

The interactive showcase — shapes with live light controls, a colour-coded FE result field, animation playback with a measured frame rate, and a stiffened cylinder — is a single file you can run straight from an IDE (right-click → Run in PyCharm) or from a shell:

python -m anytk3d.demo
python run_gui.py

The original four-viewport demo is still there:

python -m anytk3d

or embed it in an existing Tk application with anytk3d.create_stiffened_cylinder_demo(root).

Relation to ANYstructure

ANYstructure uses this package for its 3D previews. The module keeps its original API (Tkinter3DCanvas, Point3D, Camera3D and every add_* method) so it stays a drop-in dependency.

Development

pip install -e ../ANY3dView
pip install -e .[dev]
pytest

ANY3dView owns the backend-neutral geometry, camera, shading, clipping and selection types. ANYtk3D re-exports those exact objects, so existing 0.2 and 0.3 imports keep working unchanged.

The regular suite stays headless. Tests that create real Tk windows are opt-in so a developer run cannot unexpectedly open GUI sessions:

$env:ANYTK3D_RUN_GUI_TESTS = "1"
python -m pytest

Those tests use deterministic Tk-generated events and never move the desktop pointer. A smaller Windows-native acceptance suite covers event translation that synthetic Tk events cannot reproduce: hover/click and selection modifiers, directional window/crossing selection, plus middle-pan, right-orbit and wheel zoom. Run it only on an interactive Windows desktop:

$env:ANYTK3D_RUN_GUI_TESTS = "1"
$env:ANYTK3D_RUN_NATIVE_GUI = "1"
python -m pytest tests/test_native_gui_acceptance.py -q

Release qualification also reconstructs the package from Git-tracked files, builds a wheel, and imports it outside the checkout. This prevents a local, untracked runtime module from making source tests pass while a clean release fails to import.

License

Starting with version 0.5.5, ANYtk3D is licensed under the Mozilla Public License 2.0. See LICENSE. Earlier releases remain under the license terms that accompanied those releases.

See THIRD_PARTY_NOTICES.md for dependency notices.

Metadata

Release files for ANYtk3D 0.5.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ANYtk3D 0.5.5
File Size Uploaded
anytk3d-0.5.5.tar.gz 117.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ANYtk3D 0.5.5
File Interpreter ABI Platform
anytk3d-0.5.5-py3-none-any.whl Python 3 none any Details

Total release size: 203.6 kB

Release files / anytk3d-0.5.5.tar.gz

Download URL anytk3d-0.5.5.tar.gz
Size 117.2 kB
Tags Source
SHA-256 checksum
How to use checksums
52bfa2302ac4b854b3bfb168ced87b670220eb622af3a0cf081713ee9d137fe0
BLAKE2b-256 checksum
How to use checksums
7df06a6a81b991f2c8c665af46f61c8536e61911646112ec18690684a5452a5b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 3, 2026.

Transparency log

Release files / anytk3d-0.5.5-py3-none-any.whl

Download URL anytk3d-0.5.5-py3-none-any.whl
Size 86.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a1c1e662c9692c1354b6666fe166bad41b5858c361c862050e9a96bd6d2fb59a
BLAKE2b-256 checksum
How to use checksums
621c2c79e07a1ee82a7b13f888d9bf9e957a8489add7cca5df76c56d09caf01a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 3, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.5.5 This release

2 release files

0.5.3

2 release files

0.5.2

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.2.3

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page