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

GPL-3.0-or-later. See LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

anytk3d-0.5.2.tar.gz (116.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

anytk3d-0.5.2-py3-none-any.whl (92.6 kB view details)

Uploaded Python 3

File details

Details for the file anytk3d-0.5.2.tar.gz.

File metadata

  • Download URL: anytk3d-0.5.2.tar.gz
  • Upload date:
  • Size: 116.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for anytk3d-0.5.2.tar.gz
Algorithm Hash digest
SHA256 ed8688fa114970ab6c1f6eb6b229721af4b23da5c281a1101b760c0110e2bc1c
MD5 19db519aadbf674d9f8120c64d55c5d0
BLAKE2b-256 ce1a76cd1ed57bff893a7224eb3f01493bea0a1ae6449790193772d6734e7f9f

See more details on using hashes here.

Provenance

The following attestation bundles were made for anytk3d-0.5.2.tar.gz:

Publisher: release.yml on audunarn/ANYtk3D

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file anytk3d-0.5.2-py3-none-any.whl.

File metadata

  • Download URL: anytk3d-0.5.2-py3-none-any.whl
  • Upload date:
  • Size: 92.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for anytk3d-0.5.2-py3-none-any.whl
Algorithm Hash digest
SHA256 19c390eb80af425de8893ae71d1cf007101d2531a3a9760af806e8d27e4ebece
MD5 01d703788a34329ef180e98c260fcd9c
BLAKE2b-256 3690abf86fb4dd64e80eab7e8313824d003af71e08686c6f2b7fdb57c4cd2a87

See more details on using hashes here.

Provenance

The following attestation bundles were made for anytk3d-0.5.2-py3-none-any.whl:

Publisher: release.yml on audunarn/ANYtk3D

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.5.3

2 files

This release

0.5.2 This release

2 files

0.5.0

2 files

0.4.0

2 files

0.2.3

2 files

0.2.1

2 files

0.2.0

2 files

0.1.0

2 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