mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-06 20:02:16 +00:00
1f6a2d4daf
* Add a small test boat to steer around with the 3D mouse A little hull in its own camera, drawn by the UI itself and not the CAD viewport, so a 3D mouse can be tried out without opening a design. It brings the model (generated by assets/models/make_test_boat.py) and the tests for how the camera moves it. Nothing uses it yet; Preferences picks it up with the rest of the 3D mouse work. * UI kit: keep a choice row from flipping with the scrollbar A choice row picks segments or a dropdown by whether its labels fit. A page scrollbar that comes and goes narrows the row by its own width, so the row could flip back and forth between the two on a page that sits right at the edge. It is now judged on the whole row and through a small helper, fits_steadily, that only changes its answer once the miss is wider than the scrollbar. Also tests for that, for typed numbers in a slider's value box, and a test hook to put up the issue-report input barrier by hand. * 3D mouse: device model, guided calibration, buttons and a picture The 3D mouse becomes a device we know something about instead of six raw numbers. The reader picks between spacenavd and the direct hidraw driver, falls back from one to the other, and says so when it can't open the device. Guided calibration asks whether the device is a spring cap or a trackball, adds a touch step, and uses a settle filter so a ball let go of stops reading as motion. Buttons get jobs (keys, held modifiers, commands), saved per device, with defaults for the SpaceMouse Pro. Preferences gets a picture of the device that moves as it does, next to the test boat, and the same picture can sit in the 3D view at Left Viewport, Right Viewport or Left Window, above the floating History card like the legend and readouts. These ride together because the crate's calibration types, the Preferences page, the Linux reader and the button hook all use each other. * 3D mouse: offer to calibrate an uncalibrated device When a 3D mouse shows up that has not been calibrated, a card in the 3D view offers to calibrate it, to swap drivers when the other one would work better, to close, or to never ask again. It is made once per device, goes away after 30 seconds in view, and gives way to a status or computing toast instead of stacking on it. The card is the shared toast, which now takes buttons: they group by purpose, wrap inside the card at narrow widths, and do nothing while an issue report holds CAD input. The picture of the device in the 3D view keeps clear of the card. * 3D mouse: translations and attribution The 3D mouse strings (ui.3d_mouse_*, the calibration steps and the offer card) in every shipped language, the Japanese catalog note, and the credit for the test boat model in ATTRIBUTION.md. This is last because the code before it only looks keys up: it builds and runs without these, falling back to English until they land. * 3D mouse: draw its preferences with the settings kit like the rest of Navigation The 3D mouse section was a folded header with its own layout. It now uses the same kit as the sections above it: a settings_section card, settings_form rows with the shared label column, dividers between the device, axes, picture and buttons groups, Text::Hint for the helper lines, and settings_control_width for the dropdowns. The section is open like every other one (the header still folds it), so the device badge on the folded header is gone; the calibration row already names the device. Every setting, string and key is unchanged. Tests that unfolded the section no longer click it, and the hint lines are looked up as hints instead of notes. * 3D mouse: show each axis's live tilt in Preferences, like main's axis rows Each view axis row is main's axis row again: a meter that follows the tilt of the motion driving it, with the dead zone marked, next to Invert, Speed and the "driven by" menu. The meter reads the calibrated travel when the device has a calibration and the raw reading over full scale when it does not, and asks for a frame whenever the reading changes. The helpers are back as they are on main, and the calibration state is a badge by the enable toggle. * 3D mouse: line its preferences up with the rest of Navigation - The two checkboxes sit side by side in the same two-column grid as Wheel and trackpad. - Row labels stop repeating "3D mouse" and use sentence case: Calibration, Driver, Restore defaults, Reset view, Left viewport, Right viewport, Left window. - Hints line up under the control column instead of the left edge. - The six view axes are named Pan X, Pan Y, Zoom, Orbit X, Orbit Y and Roll so they fit the name column. - Buttons are a divider and rows in a form like the other groups, without the bold sub-heading. - The driver, dead zone, axes, picture and buttons sit behind a collapsed Advanced section, since most people never touch them. * 3D mouse: tidy the Advanced test for clippy
80 lines
3.7 KiB
Python
80 lines
3.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Packs the boat for the 3D mouse test model in Preferences into assets/models/test_boat.bin, from a
|
|
binary STL of the low-poly boat (about 850 triangles). Needs numpy.
|
|
|
|
python3 assets/models/make_test_boat.py SOURCE.stl assets/models/test_boat.bin
|
|
|
|
No reduction happens here: the vertices are welded, and each welded vertex is split by the angle
|
|
between the faces that meet there (more than 40 degrees apart is a crease), so smooth surfaces share
|
|
a vertex normal and edges stay sharp. The model is centred and scaled into a unit box, then
|
|
quantised. File layout, little-endian: u32 vertex count, u32 triangle count, f32 centre (x, y, z),
|
|
f32 half size of the longest side; then per vertex its position as i16 x 3 (32767 is the half size)
|
|
and its normal as i8 x 3 (127 is a unit); then per triangle three u16 vertex indices. The model
|
|
sits with z up.
|
|
"""
|
|
import struct
|
|
import sys
|
|
|
|
import numpy as np
|
|
|
|
CREASE_DEGREES = 40.0
|
|
|
|
|
|
def main() -> None:
|
|
source, target = sys.argv[1], sys.argv[2]
|
|
data = open(source, "rb").read()
|
|
count = struct.unpack_from("<I", data, 80)[0]
|
|
record = np.dtype([("n", "<f4", 3), ("v", "<f4", (3, 3)), ("attr", "<u2")])
|
|
tris = np.frombuffer(data, dtype=record, count=count, offset=84)["v"].astype(np.float64)
|
|
points, inverse = np.unique(np.round(tris.reshape(-1, 3), 3), axis=0, return_inverse=True)
|
|
faces = inverse.reshape(-1, 3)
|
|
keep = (faces[:, 0] != faces[:, 1]) & (faces[:, 1] != faces[:, 2]) & (faces[:, 0] != faces[:, 2])
|
|
faces = faces[keep]
|
|
a, b, c = points[faces[:, 0]], points[faces[:, 1]], points[faces[:, 2]]
|
|
cross = np.cross(b - a, c - a)
|
|
area = np.linalg.norm(cross, axis=1)
|
|
ok = area > 1e-9
|
|
faces, cross, area = faces[ok], cross[ok], area[ok]
|
|
face_normal = cross / area[:, None]
|
|
# Split each welded vertex into one vertex per group of faces that agree (within the crease angle).
|
|
cos_limit = np.cos(np.radians(CREASE_DEGREES))
|
|
corners = {}
|
|
for f, tri in enumerate(faces):
|
|
for k in range(3):
|
|
corners.setdefault(int(tri[k]), []).append((f, k))
|
|
out_points, out_normals, index = [], [], np.zeros(faces.shape, dtype=np.int64)
|
|
for v, incident in corners.items():
|
|
groups = [] # each: [summed normal, [(face, corner)]]
|
|
for f, k in incident:
|
|
n = face_normal[f]
|
|
for g in groups:
|
|
mean = g[0] / np.linalg.norm(g[0])
|
|
if mean @ n > cos_limit:
|
|
g[0] = g[0] + n * area[f]
|
|
g[1].append((f, k))
|
|
break
|
|
else:
|
|
groups.append([n * area[f], [(f, k)]])
|
|
for total, members in groups:
|
|
length = np.linalg.norm(total)
|
|
out_normals.append(total / length if length > 0 else np.array([0.0, 0.0, 1.0]))
|
|
out_points.append(points[v])
|
|
for f, k in members:
|
|
index[f, k] = len(out_points) - 1
|
|
out_points, out_normals = np.array(out_points), np.array(out_normals)
|
|
low, high = out_points.min(axis=0), out_points.max(axis=0)
|
|
centre, half = (low + high) / 2.0, float((high - low).max() / 2.0)
|
|
positions = np.clip(np.round((out_points - centre) / half * 32767.0), -32767, 32767).astype("<i2")
|
|
normals = np.clip(np.round(out_normals * 127.0), -127, 127).astype("i1")
|
|
assert len(out_points) <= 65535
|
|
with open(target, "wb") as out:
|
|
out.write(struct.pack("<II3ff", len(out_points), len(index), *centre.tolist(), half))
|
|
for p, n in zip(positions, normals):
|
|
out.write(p.tobytes() + n.tobytes())
|
|
out.write(index.astype("<u2").tobytes())
|
|
print(f"{len(out_points)} vertices, {len(index)} triangles, centre {centre}, half size {half}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|