mirror of
https://github.com/panda3d/panda3d.git
synced 2025-09-29 16:20:11 -04:00
Merge branch 'release/1.10.x'
This commit is contained in:
commit
36dd8889e7
@ -12,6 +12,7 @@
|
||||
*/
|
||||
|
||||
#include "configVariableBool.h"
|
||||
#include "mutexImpl.h"
|
||||
|
||||
/**
|
||||
* Refreshes the cached value.
|
||||
@ -21,7 +22,10 @@ reload_value() const {
|
||||
// NB. MSVC doesn't guarantee that this mutex is initialized in a
|
||||
// thread-safe manner. But chances are that the first time this is called
|
||||
// is at static init time, when there is no risk of data races.
|
||||
static MutexImpl lock;
|
||||
|
||||
// This needs to be a recursive mutex, because get_bool_word() may end up
|
||||
// indirectly querying another bool config variable.
|
||||
static ReMutexImpl lock;
|
||||
lock.lock();
|
||||
|
||||
// We check again for cache validity since another thread may have beaten
|
||||
|
@ -288,6 +288,119 @@ test_intersection_from_segment(const CollisionEntry &entry) const {
|
||||
return new_entry;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
PT(CollisionEntry) CollisionInvSphere::
|
||||
test_intersection_from_capsule(const CollisionEntry &entry) const {
|
||||
const CollisionCapsule *capsule;
|
||||
DCAST_INTO_R(capsule, entry.get_from(), nullptr);
|
||||
|
||||
const LMatrix4 &wrt_mat = entry.get_wrt_mat();
|
||||
|
||||
LPoint3 from_a = capsule->get_point_a() * wrt_mat;
|
||||
LPoint3 from_b = capsule->get_point_b() * wrt_mat;
|
||||
|
||||
LVector3 from_radius_v =
|
||||
LVector3(capsule->get_radius(), 0.0f, 0.0f) * wrt_mat;
|
||||
PN_stdfloat from_radius = from_radius_v.length();
|
||||
|
||||
LPoint3 center = get_center();
|
||||
PN_stdfloat radius = get_radius();
|
||||
|
||||
// Check which one of the points lies furthest inside the sphere.
|
||||
PN_stdfloat dist_a = (from_a - center).length();
|
||||
PN_stdfloat dist_b = (from_b - center).length();
|
||||
if (dist_b > dist_a) {
|
||||
// Store the furthest point into from_a/dist_a.
|
||||
dist_a = dist_b;
|
||||
from_a = from_b;
|
||||
}
|
||||
|
||||
// from_a now contains the furthest point. Is it inside?
|
||||
if (dist_a < radius - from_radius) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (collide_cat.is_debug()) {
|
||||
collide_cat.debug()
|
||||
<< "intersection detected from " << entry.get_from_node_path()
|
||||
<< " into " << entry.get_into_node_path() << "\n";
|
||||
}
|
||||
PT(CollisionEntry) new_entry = new CollisionEntry(entry);
|
||||
|
||||
LVector3 normal = center - from_a;
|
||||
normal.normalize();
|
||||
new_entry->set_surface_point(get_center() - normal * radius);
|
||||
new_entry->set_interior_point(from_a - normal * from_radius);
|
||||
|
||||
if (has_effective_normal() && capsule->get_respect_effective_normal()) {
|
||||
new_entry->set_surface_normal(get_effective_normal());
|
||||
} else {
|
||||
new_entry->set_surface_normal(normal);
|
||||
}
|
||||
|
||||
return new_entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Double dispatch point for box as a FROM object
|
||||
*/
|
||||
PT(CollisionEntry) CollisionInvSphere::
|
||||
test_intersection_from_box(const CollisionEntry &entry) const {
|
||||
const CollisionBox *box;
|
||||
DCAST_INTO_R(box, entry.get_from(), nullptr);
|
||||
|
||||
const LMatrix4 &wrt_mat = entry.get_wrt_mat();
|
||||
|
||||
LPoint3 center = get_center();
|
||||
PN_stdfloat radius_sq = get_radius();
|
||||
radius_sq *= radius_sq;
|
||||
|
||||
// Just figure out which box point is furthest from the center. If it
|
||||
// exceeds the radius, the furthest point wins.
|
||||
|
||||
PN_stdfloat max_dist_sq = -1.0;
|
||||
LPoint3 deepest_vertex;
|
||||
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
LPoint3 point = wrt_mat.xform_point(box->get_point(i));
|
||||
|
||||
PN_stdfloat dist_sq = (point - center).length_squared();
|
||||
if (dist_sq > max_dist_sq) {
|
||||
deepest_vertex = point;
|
||||
max_dist_sq = dist_sq;
|
||||
}
|
||||
}
|
||||
|
||||
if (max_dist_sq < radius_sq) {
|
||||
// The point furthest away from the center is still inside the sphere.
|
||||
// Therefore, no collision.
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (collide_cat.is_debug()) {
|
||||
collide_cat.debug()
|
||||
<< "intersection detected from " << entry.get_from_node_path()
|
||||
<< " into " << entry.get_into_node_path() << "\n";
|
||||
}
|
||||
|
||||
PT(CollisionEntry) new_entry = new CollisionEntry(entry);
|
||||
|
||||
// The interior point is just the deepest cube vertex.
|
||||
new_entry->set_interior_point(deepest_vertex);
|
||||
|
||||
// Now extrapolate the surface point and normal from that.
|
||||
LVector3 normal = center - deepest_vertex;
|
||||
normal.normalize();
|
||||
new_entry->set_surface_point(center - normal * get_radius());
|
||||
new_entry->set_surface_normal(
|
||||
(has_effective_normal() && box->get_respect_effective_normal())
|
||||
? get_effective_normal() : normal);
|
||||
|
||||
return new_entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the _viz_geom GeomNode up with Geoms suitable for rendering this
|
||||
* solid.
|
||||
|
@ -55,6 +55,10 @@ protected:
|
||||
test_intersection_from_ray(const CollisionEntry &entry) const;
|
||||
virtual PT(CollisionEntry)
|
||||
test_intersection_from_segment(const CollisionEntry &entry) const;
|
||||
virtual PT(CollisionEntry)
|
||||
test_intersection_from_capsule(const CollisionEntry &entry) const;
|
||||
virtual PT(CollisionEntry)
|
||||
test_intersection_from_box(const CollisionEntry &entry) const;
|
||||
|
||||
virtual void fill_viz_geom();
|
||||
|
||||
|
@ -82,7 +82,7 @@ static const struct DeviceMapping {
|
||||
{0x28de, 0x1142, InputDevice::DeviceClass::unknown, QB_steam_controller},
|
||||
// Jess Tech Colour Rumble Pad
|
||||
{0x0f30, 0x0111, InputDevice::DeviceClass::gamepad, 0},
|
||||
// Trust GXT 24
|
||||
// SPEED Link SL-6535-SBK-01
|
||||
{0x0079, 0x0006, InputDevice::DeviceClass::gamepad, 0},
|
||||
// 3Dconnexion Space Traveller 3D Mouse
|
||||
{0x046d, 0xc623, InputDevice::DeviceClass::spatial_mouse, 0},
|
||||
@ -521,7 +521,10 @@ init_device() {
|
||||
}
|
||||
break;
|
||||
case ABS_THROTTLE:
|
||||
if (quirks & QB_rudder_from_throttle) {
|
||||
if (_device_class == DeviceClass::gamepad) {
|
||||
// Apparently needed for 8bitdo N30 Pro controller
|
||||
axis = InputDevice::Axis::right_x;
|
||||
} else if (quirks & QB_rudder_from_throttle) {
|
||||
axis = InputDevice::Axis::rudder;
|
||||
} else {
|
||||
axis = InputDevice::Axis::throttle;
|
||||
|
166
panda/src/device/phidsdi.h
Normal file
166
panda/src/device/phidsdi.h
Normal file
@ -0,0 +1,166 @@
|
||||
/**
|
||||
* PANDA 3D SOFTWARE
|
||||
* Copyright (c) Carnegie Mellon University. All rights reserved.
|
||||
*
|
||||
* All use of this software is subject to the terms of the revised BSD
|
||||
* license. You should have received a copy of this license along
|
||||
* with this source code in a file named "LICENSE."
|
||||
*
|
||||
* @file phidsdi.h
|
||||
* @author rdb
|
||||
* @date 2019-02-05
|
||||
*/
|
||||
|
||||
#ifndef PHIDSDI_H
|
||||
#define PHIDSDI_H
|
||||
|
||||
#if defined(_WIN32) && !defined(CPPPARSER)
|
||||
|
||||
// Copy definitions from hidusage.h, until we can drop support for the 7.1 SDK
|
||||
typedef USHORT USAGE, *PUSAGE;
|
||||
|
||||
#define HID_USAGE_PAGE_UNDEFINED ((USAGE) 0x00)
|
||||
#define HID_USAGE_PAGE_GENERIC ((USAGE) 0x01)
|
||||
#define HID_USAGE_PAGE_SIMULATION ((USAGE) 0x02)
|
||||
#define HID_USAGE_PAGE_VR ((USAGE) 0x03)
|
||||
#define HID_USAGE_PAGE_SPORT ((USAGE) 0x04)
|
||||
#define HID_USAGE_PAGE_GAME ((USAGE) 0x05)
|
||||
#define HID_USAGE_PAGE_KEYBOARD ((USAGE) 0x07)
|
||||
#define HID_USAGE_PAGE_LED ((USAGE) 0x08)
|
||||
#define HID_USAGE_PAGE_BUTTON ((USAGE) 0x09)
|
||||
|
||||
#define HID_USAGE_GENERIC_POINTER ((USAGE) 0x01)
|
||||
#define HID_USAGE_GENERIC_MOUSE ((USAGE) 0x02)
|
||||
#define HID_USAGE_GENERIC_JOYSTICK ((USAGE) 0x04)
|
||||
#define HID_USAGE_GENERIC_GAMEPAD ((USAGE) 0x05)
|
||||
#define HID_USAGE_GENERIC_KEYBOARD ((USAGE) 0x06)
|
||||
#define HID_USAGE_GENERIC_KEYPAD ((USAGE) 0x07)
|
||||
#define HID_USAGE_GENERIC_SYSTEM_CTL ((USAGE) 0x80)
|
||||
|
||||
#define HID_USAGE_GENERIC_X ((USAGE) 0x30)
|
||||
#define HID_USAGE_GENERIC_Y ((USAGE) 0x31)
|
||||
#define HID_USAGE_GENERIC_Z ((USAGE) 0x32)
|
||||
#define HID_USAGE_GENERIC_RX ((USAGE) 0x33)
|
||||
#define HID_USAGE_GENERIC_RY ((USAGE) 0x34)
|
||||
#define HID_USAGE_GENERIC_RZ ((USAGE) 0x35)
|
||||
#define HID_USAGE_GENERIC_SLIDER ((USAGE) 0x36)
|
||||
#define HID_USAGE_GENERIC_DIAL ((USAGE) 0x37)
|
||||
#define HID_USAGE_GENERIC_WHEEL ((USAGE) 0x38)
|
||||
#define HID_USAGE_GENERIC_HATSWITCH ((USAGE) 0x39)
|
||||
|
||||
// Copy definitions from hidpi.h, until we can drop support for the 7.1 SDK
|
||||
#define HIDP_STATUS_SUCCESS ((NTSTATUS)(0x11 << 16))
|
||||
|
||||
typedef enum _HIDP_REPORT_TYPE {
|
||||
HidP_Input,
|
||||
HidP_Output,
|
||||
HidP_Feature
|
||||
} HIDP_REPORT_TYPE;
|
||||
|
||||
typedef struct _HIDP_BUTTON_CAPS {
|
||||
USAGE UsagePage;
|
||||
UCHAR ReportID;
|
||||
BOOLEAN IsAlias;
|
||||
USHORT BitField;
|
||||
USHORT LinkCollection;
|
||||
USAGE LinkUsage;
|
||||
USAGE LinkUsagePage;
|
||||
BOOLEAN IsRange;
|
||||
BOOLEAN IsStringRange;
|
||||
BOOLEAN IsDesignatorRange;
|
||||
BOOLEAN IsAbsolute;
|
||||
ULONG Reserved[10];
|
||||
union {
|
||||
struct {
|
||||
USAGE UsageMin, UsageMax;
|
||||
USHORT StringMin, StringMax;
|
||||
USHORT DesignatorMin, DesignatorMax;
|
||||
USHORT DataIndexMin, DataIndexMax;
|
||||
} Range;
|
||||
struct {
|
||||
USAGE Usage, Reserved1;
|
||||
USHORT StringIndex, Reserved2;
|
||||
USHORT DesignatorIndex, Reserved3;
|
||||
USHORT DataIndex, Reserved4;
|
||||
} NotRange;
|
||||
};
|
||||
} HIDP_BUTTON_CAPS, *PHIDP_BUTTON_CAPS;
|
||||
|
||||
typedef struct _HIDP_VALUE_CAPS {
|
||||
USAGE UsagePage;
|
||||
UCHAR ReportID;
|
||||
BOOLEAN IsAlias;
|
||||
USHORT BitField;
|
||||
USHORT LinkCollection;
|
||||
USAGE LinkUsage;
|
||||
USAGE LinkUsagePage;
|
||||
BOOLEAN IsRange;
|
||||
BOOLEAN IsStringRange;
|
||||
BOOLEAN IsDesignatorRange;
|
||||
BOOLEAN IsAbsolute;
|
||||
BOOLEAN HasNull;
|
||||
UCHAR Reserved;
|
||||
USHORT BitSize;
|
||||
USHORT ReportCount;
|
||||
USHORT Reserved2[5];
|
||||
ULONG UnitsExp;
|
||||
ULONG Units;
|
||||
LONG LogicalMin, LogicalMax;
|
||||
LONG PhysicalMin, PhysicalMax;
|
||||
union {
|
||||
struct {
|
||||
USAGE UsageMin, UsageMax;
|
||||
USHORT StringMin, StringMax;
|
||||
USHORT DesignatorMin, DesignatorMax;
|
||||
USHORT DataIndexMin, DataIndexMax;
|
||||
} Range;
|
||||
struct {
|
||||
USAGE Usage, Reserved1;
|
||||
USHORT StringIndex, Reserved2;
|
||||
USHORT DesignatorIndex, Reserved3;
|
||||
USHORT DataIndex, Reserved4;
|
||||
} NotRange;
|
||||
};
|
||||
} HIDP_VALUE_CAPS, *PHIDP_VALUE_CAPS;
|
||||
|
||||
typedef PUCHAR PHIDP_REPORT_DESCRIPTOR;
|
||||
typedef struct _HIDP_PREPARSED_DATA *PHIDP_PREPARSED_DATA;
|
||||
|
||||
typedef struct _HIDP_CAPS {
|
||||
USAGE Usage;
|
||||
USAGE UsagePage;
|
||||
USHORT InputReportByteLength;
|
||||
USHORT OutputReportByteLength;
|
||||
USHORT FeatureReportByteLength;
|
||||
USHORT Reserved[17];
|
||||
USHORT NumberLinkCollectionNodes;
|
||||
USHORT NumberInputButtonCaps;
|
||||
USHORT NumberInputValueCaps;
|
||||
USHORT NumberInputDataIndices;
|
||||
USHORT NumberOutputButtonCaps;
|
||||
USHORT NumberOutputValueCaps;
|
||||
USHORT NumberOutputDataIndices;
|
||||
USHORT NumberFeatureButtonCaps;
|
||||
USHORT NumberFeatureValueCaps;
|
||||
USHORT NumberFeatureDataIndices;
|
||||
} HIDP_CAPS, *PHIDP_CAPS;
|
||||
|
||||
typedef struct _HIDP_DATA {
|
||||
USHORT DataIndex;
|
||||
USHORT Reserved;
|
||||
union {
|
||||
ULONG RawValue;
|
||||
BOOLEAN On;
|
||||
};
|
||||
} HIDP_DATA, *PHIDP_DATA;
|
||||
|
||||
typedef LONG NTSTATUS;
|
||||
typedef NTSTATUS (__stdcall *pHidP_GetCaps)(PHIDP_PREPARSED_DATA, PHIDP_CAPS);
|
||||
typedef NTSTATUS (__stdcall *pHidP_GetButtonCaps)(HIDP_REPORT_TYPE, PHIDP_BUTTON_CAPS, PUSHORT, PHIDP_PREPARSED_DATA);
|
||||
typedef NTSTATUS (__stdcall *pHidP_GetValueCaps)(HIDP_REPORT_TYPE, PHIDP_VALUE_CAPS, PUSHORT, PHIDP_PREPARSED_DATA);
|
||||
typedef NTSTATUS (__stdcall *pHidP_GetData)(HIDP_REPORT_TYPE, PHIDP_DATA, PULONG, PHIDP_PREPARSED_DATA, PCHAR, ULONG);
|
||||
typedef ULONG (__stdcall *pHidP_MaxDataListLength)(HIDP_REPORT_TYPE, PHIDP_PREPARSED_DATA);
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
@ -289,8 +289,8 @@ on_input_device_arrival(HANDLE handle) {
|
||||
|
||||
// Some devices insert quite some trailing space here.
|
||||
wchar_t *wbuffer = (wchar_t *)buffer;
|
||||
size_t wlen = wcslen(wbuffer);
|
||||
while (iswspace(wbuffer[wlen - 1])) {
|
||||
size_t wlen = wcsnlen_s(wbuffer, sizeof(buffer) / sizeof(wchar_t));
|
||||
while (wlen > 0 && iswspace(wbuffer[wlen - 1])) {
|
||||
wbuffer[--wlen] = 0;
|
||||
}
|
||||
TextEncoder encoder;
|
||||
@ -391,6 +391,19 @@ on_input_device_removal(HANDLE handle) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls the system to see if there are any new devices. In some
|
||||
* implementations this is a no-op.
|
||||
*/
|
||||
void WinInputDeviceManager::
|
||||
update() {
|
||||
MSG msg;
|
||||
while (PeekMessage(&msg, _message_hwnd, WM_INPUT_DEVICE_CHANGE, WM_INPUT, PM_REMOVE)) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of the message loop.
|
||||
*/
|
||||
|
@ -52,9 +52,11 @@ private:
|
||||
pmap<HANDLE, WinRawInputDevice *> _raw_devices;
|
||||
pmap<std::string, WinRawInputDevice *> _raw_devices_by_path;
|
||||
|
||||
virtual void update() override;
|
||||
|
||||
static LRESULT WINAPI window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
|
||||
|
||||
typedef CONFIGRET (*pCM_Get_DevNode_Property)(DEVINST, const DEVPROPKEY *, DEVPROPTYPE *, PBYTE, PULONG, ULONG);
|
||||
typedef CONFIGRET (WINAPI *pCM_Get_DevNode_Property)(DEVINST, const DEVPROPKEY *, DEVPROPTYPE *, PBYTE, PULONG, ULONG);
|
||||
pCM_Get_DevNode_Property _CM_Get_DevNode_PropertyW;
|
||||
|
||||
friend class InputDeviceManager;
|
||||
|
@ -14,156 +14,56 @@
|
||||
#include "winRawInputDevice.h"
|
||||
#include "gamepadButton.h"
|
||||
#include "mouseButton.h"
|
||||
#include "buttonRegistry.h"
|
||||
|
||||
#if defined(_WIN32) && !defined(CPPPARSER)
|
||||
|
||||
#include <CfgMgr32.h>
|
||||
#include <devpkey.h>
|
||||
#include "phidsdi.h"
|
||||
|
||||
// Copy definitions from hidusage.h, until we can drop support for the 7.1 SDK
|
||||
typedef USHORT USAGE, *PUSAGE;
|
||||
enum QuirkBits : int {
|
||||
// Has no trigger axes.
|
||||
QB_no_analog_triggers = 1,
|
||||
|
||||
#define HID_USAGE_PAGE_UNDEFINED ((USAGE) 0x00)
|
||||
#define HID_USAGE_PAGE_GENERIC ((USAGE) 0x01)
|
||||
#define HID_USAGE_PAGE_SIMULATION ((USAGE) 0x02)
|
||||
#define HID_USAGE_PAGE_VR ((USAGE) 0x03)
|
||||
#define HID_USAGE_PAGE_SPORT ((USAGE) 0x04)
|
||||
#define HID_USAGE_PAGE_GAME ((USAGE) 0x05)
|
||||
#define HID_USAGE_PAGE_KEYBOARD ((USAGE) 0x07)
|
||||
#define HID_USAGE_PAGE_LED ((USAGE) 0x08)
|
||||
#define HID_USAGE_PAGE_BUTTON ((USAGE) 0x09)
|
||||
// Throttle goes from -1 to 1 rather than from 0 to 1.
|
||||
QB_centered_throttle = 2,
|
||||
|
||||
#define HID_USAGE_GENERIC_POINTER ((USAGE) 0x01)
|
||||
#define HID_USAGE_GENERIC_MOUSE ((USAGE) 0x02)
|
||||
#define HID_USAGE_GENERIC_JOYSTICK ((USAGE) 0x04)
|
||||
#define HID_USAGE_GENERIC_GAMEPAD ((USAGE) 0x05)
|
||||
#define HID_USAGE_GENERIC_KEYBOARD ((USAGE) 0x06)
|
||||
#define HID_USAGE_GENERIC_KEYPAD ((USAGE) 0x07)
|
||||
#define HID_USAGE_GENERIC_SYSTEM_CTL ((USAGE) 0x80)
|
||||
// Throttle is reversed.
|
||||
QB_reversed_throttle = 4,
|
||||
};
|
||||
|
||||
#define HID_USAGE_GENERIC_X ((USAGE) 0x30)
|
||||
#define HID_USAGE_GENERIC_Y ((USAGE) 0x31)
|
||||
#define HID_USAGE_GENERIC_Z ((USAGE) 0x32)
|
||||
#define HID_USAGE_GENERIC_RX ((USAGE) 0x33)
|
||||
#define HID_USAGE_GENERIC_RY ((USAGE) 0x34)
|
||||
#define HID_USAGE_GENERIC_RZ ((USAGE) 0x35)
|
||||
#define HID_USAGE_GENERIC_SLIDER ((USAGE) 0x36)
|
||||
#define HID_USAGE_GENERIC_DIAL ((USAGE) 0x37)
|
||||
#define HID_USAGE_GENERIC_WHEEL ((USAGE) 0x38)
|
||||
#define HID_USAGE_GENERIC_HATSWITCH ((USAGE) 0x39)
|
||||
// Some nonstandard gamepads have different button mappings.
|
||||
static const struct DeviceMapping {
|
||||
unsigned short vendor;
|
||||
unsigned short product;
|
||||
InputDevice::DeviceClass device_class;
|
||||
int quirks;
|
||||
const char *buttons[16];
|
||||
} mapping_presets[] = {
|
||||
// SNES-style USB gamepad
|
||||
{0x0810, 0xe501, InputDevice::DeviceClass::gamepad, QB_no_analog_triggers,
|
||||
{"face_x", "face_a", "face_b", "face_y", "lshoulder", "rshoulder", "none", "none", "back", "start"}
|
||||
},
|
||||
// SPEED Link SL-6535-SBK-01
|
||||
{0x0079, 0x0006, InputDevice::DeviceClass::gamepad, QB_no_analog_triggers,
|
||||
{"face_y", "face_b", "face_a", "face_x", "lshoulder", "rshoulder", "ltrigger", "rtrigger", "back", "start", "lstick", "rstick"}
|
||||
},
|
||||
// T.Flight Hotas X
|
||||
{0x044f, 0xb108, InputDevice::DeviceClass::flight_stick, QB_centered_throttle | QB_reversed_throttle,
|
||||
{0}
|
||||
},
|
||||
// NVIDIA Shield Controller
|
||||
{0x0955, 0x7214, InputDevice::DeviceClass::gamepad, 0,
|
||||
{"face_a", "face_b", "n", "face_x", "face_y", "rshoulder", "lshoulder", "rshoulder", "e", "f", "g", "start", "h", "lstick", "rstick", "i"}
|
||||
},
|
||||
{0},
|
||||
};
|
||||
|
||||
// Copy definitions from hidpi.h, until we can drop support for the 7.1 SDK
|
||||
#define HIDP_STATUS_SUCCESS ((NTSTATUS)(0x11 << 16))
|
||||
|
||||
typedef enum _HIDP_REPORT_TYPE {
|
||||
HidP_Input,
|
||||
HidP_Output,
|
||||
HidP_Feature
|
||||
} HIDP_REPORT_TYPE;
|
||||
|
||||
typedef struct _HIDP_BUTTON_CAPS {
|
||||
USAGE UsagePage;
|
||||
UCHAR ReportID;
|
||||
BOOLEAN IsAlias;
|
||||
USHORT BitField;
|
||||
USHORT LinkCollection;
|
||||
USAGE LinkUsage;
|
||||
USAGE LinkUsagePage;
|
||||
BOOLEAN IsRange;
|
||||
BOOLEAN IsStringRange;
|
||||
BOOLEAN IsDesignatorRange;
|
||||
BOOLEAN IsAbsolute;
|
||||
ULONG Reserved[10];
|
||||
union {
|
||||
struct {
|
||||
USAGE UsageMin, UsageMax;
|
||||
USHORT StringMin, StringMax;
|
||||
USHORT DesignatorMin, DesignatorMax;
|
||||
USHORT DataIndexMin, DataIndexMax;
|
||||
} Range;
|
||||
struct {
|
||||
USAGE Usage, Reserved1;
|
||||
USHORT StringIndex, Reserved2;
|
||||
USHORT DesignatorIndex, Reserved3;
|
||||
USHORT DataIndex, Reserved4;
|
||||
} NotRange;
|
||||
};
|
||||
} HIDP_BUTTON_CAPS, *PHIDP_BUTTON_CAPS;
|
||||
|
||||
typedef struct _HIDP_VALUE_CAPS {
|
||||
USAGE UsagePage;
|
||||
UCHAR ReportID;
|
||||
BOOLEAN IsAlias;
|
||||
USHORT BitField;
|
||||
USHORT LinkCollection;
|
||||
USAGE LinkUsage;
|
||||
USAGE LinkUsagePage;
|
||||
BOOLEAN IsRange;
|
||||
BOOLEAN IsStringRange;
|
||||
BOOLEAN IsDesignatorRange;
|
||||
BOOLEAN IsAbsolute;
|
||||
BOOLEAN HasNull;
|
||||
UCHAR Reserved;
|
||||
USHORT BitSize;
|
||||
USHORT ReportCount;
|
||||
USHORT Reserved2[5];
|
||||
ULONG UnitsExp;
|
||||
ULONG Units;
|
||||
LONG LogicalMin, LogicalMax;
|
||||
LONG PhysicalMin, PhysicalMax;
|
||||
union {
|
||||
struct {
|
||||
USAGE UsageMin, UsageMax;
|
||||
USHORT StringMin, StringMax;
|
||||
USHORT DesignatorMin, DesignatorMax;
|
||||
USHORT DataIndexMin, DataIndexMax;
|
||||
} Range;
|
||||
struct {
|
||||
USAGE Usage, Reserved1;
|
||||
USHORT StringIndex, Reserved2;
|
||||
USHORT DesignatorIndex, Reserved3;
|
||||
USHORT DataIndex, Reserved4;
|
||||
} NotRange;
|
||||
};
|
||||
} HIDP_VALUE_CAPS, *PHIDP_VALUE_CAPS;
|
||||
|
||||
typedef PUCHAR PHIDP_REPORT_DESCRIPTOR;
|
||||
typedef struct _HIDP_PREPARSED_DATA *PHIDP_PREPARSED_DATA;
|
||||
|
||||
typedef struct _HIDP_CAPS {
|
||||
USAGE Usage;
|
||||
USAGE UsagePage;
|
||||
USHORT InputReportByteLength;
|
||||
USHORT OutputReportByteLength;
|
||||
USHORT FeatureReportByteLength;
|
||||
USHORT Reserved[17];
|
||||
USHORT NumberLinkCollectionNodes;
|
||||
USHORT NumberInputButtonCaps;
|
||||
USHORT NumberInputValueCaps;
|
||||
USHORT NumberInputDataIndices;
|
||||
USHORT NumberOutputButtonCaps;
|
||||
USHORT NumberOutputValueCaps;
|
||||
USHORT NumberOutputDataIndices;
|
||||
USHORT NumberFeatureButtonCaps;
|
||||
USHORT NumberFeatureValueCaps;
|
||||
USHORT NumberFeatureDataIndices;
|
||||
} HIDP_CAPS, *PHIDP_CAPS;
|
||||
|
||||
typedef struct _HIDP_DATA {
|
||||
USHORT DataIndex;
|
||||
USHORT Reserved;
|
||||
union {
|
||||
ULONG RawValue;
|
||||
BOOLEAN On;
|
||||
};
|
||||
} HIDP_DATA, *PHIDP_DATA;
|
||||
|
||||
typedef LONG NTSTATUS;
|
||||
typedef NTSTATUS (*pHidP_GetCaps)(PHIDP_PREPARSED_DATA, PHIDP_CAPS);
|
||||
typedef NTSTATUS (*pHidP_GetButtonCaps)(HIDP_REPORT_TYPE, PHIDP_BUTTON_CAPS, PUSHORT, PHIDP_PREPARSED_DATA);
|
||||
typedef NTSTATUS (*pHidP_GetValueCaps)(HIDP_REPORT_TYPE, PHIDP_VALUE_CAPS, PUSHORT, PHIDP_PREPARSED_DATA);
|
||||
typedef NTSTATUS (*pHidP_GetData)(HIDP_REPORT_TYPE, PHIDP_DATA, PULONG, PHIDP_PREPARSED_DATA, PCHAR, ULONG);
|
||||
typedef ULONG (*pHidP_MaxDataListLength)(HIDP_REPORT_TYPE, PHIDP_PREPARSED_DATA);
|
||||
// This is our fallback button mapping, used with Xbox 360 and other devices.
|
||||
static const char *default_gamepad_mapping[16] = {
|
||||
"face_a", "face_b", "face_x", "face_y", "lshoulder", "rshoulder", "back", "start", "lstick", "rstick"
|
||||
};
|
||||
|
||||
static pHidP_GetCaps _HidP_GetCaps = nullptr;
|
||||
static pHidP_GetButtonCaps _HidP_GetButtonCaps = nullptr;
|
||||
@ -246,6 +146,9 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
|
||||
|
||||
_name = std::move(name);
|
||||
|
||||
int quirks = 0;
|
||||
const char *const *gamepad_buttons = default_gamepad_mapping;
|
||||
|
||||
switch (info.dwType) {
|
||||
case RIM_TYPEMOUSE:
|
||||
_device_class = DeviceClass::mouse;
|
||||
@ -264,7 +167,7 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
|
||||
info.hid.usUsage == HID_USAGE_GENERIC_GAMEPAD) {
|
||||
_device_class = DeviceClass::gamepad;
|
||||
|
||||
// Flight sticks
|
||||
// Various game controllers, incl. flight sticks and some gamepads
|
||||
} else if (info.hid.usUsagePage == HID_USAGE_PAGE_GENERIC &&
|
||||
info.hid.usUsage == HID_USAGE_GENERIC_JOYSTICK) {
|
||||
_device_class = DeviceClass::flight_stick;
|
||||
@ -273,11 +176,6 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
|
||||
// Well, it claims to be a gamepad...
|
||||
_device_class = DeviceClass::gamepad;
|
||||
}
|
||||
//TODO: better solution for this
|
||||
if (_vendor_id == 0x0079 && _product_id == 0x0006) {
|
||||
// Trust GXT 24
|
||||
_device_class = DeviceClass::gamepad;
|
||||
}
|
||||
|
||||
// Mice
|
||||
} else if (info.hid.usUsagePage == HID_USAGE_PAGE_GENERIC &&
|
||||
@ -306,6 +204,28 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_device_class == DeviceClass::gamepad ||
|
||||
_device_class == DeviceClass::flight_stick) {
|
||||
// Do we have a built-in mapping?
|
||||
const DeviceMapping *mapping = mapping_presets;
|
||||
while (mapping->vendor != 0) {
|
||||
if (info.hid.dwVendorId == mapping->vendor &&
|
||||
info.hid.dwProductId == mapping->product) {
|
||||
_device_class = mapping->device_class;
|
||||
gamepad_buttons = mapping->buttons;
|
||||
quirks = mapping->quirks;
|
||||
if (device_cat.is_debug()) {
|
||||
device_cat.debug()
|
||||
<< "Using preset mapping for " << mapping->device_class
|
||||
<< " with VID=" << std::hex << mapping->vendor
|
||||
<< " PID=" << mapping->product << std::dec << "\n";
|
||||
}
|
||||
break;
|
||||
}
|
||||
++mapping;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize hid.dll, which provides the HID parser functions.
|
||||
static bool hid_initialized = false;
|
||||
if (!hid_initialized) {
|
||||
@ -342,38 +262,7 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
|
||||
<< caps.NumberInputValueCaps << " value caps\n";
|
||||
}
|
||||
|
||||
// Do we have a button mapping?
|
||||
static const ButtonHandle gamepad_buttons_common[] = {
|
||||
ButtonHandle::none(),
|
||||
GamepadButton::face_a(),
|
||||
GamepadButton::face_b(),
|
||||
GamepadButton::face_x(),
|
||||
GamepadButton::face_y(),
|
||||
GamepadButton::lshoulder(),
|
||||
GamepadButton::rshoulder(),
|
||||
GamepadButton::start(),
|
||||
GamepadButton::back(),
|
||||
GamepadButton::lstick(),
|
||||
GamepadButton::rstick(),
|
||||
};
|
||||
const ButtonHandle *gamepad_buttons = gamepad_buttons_common;
|
||||
if (_vendor_id == 0x0810 && _product_id == 0xe501) {
|
||||
// SNES-style USB gamepad
|
||||
static const ButtonHandle gamepad_buttons_snes[] = {
|
||||
ButtonHandle::none(),
|
||||
GamepadButton::face_x(),
|
||||
GamepadButton::face_a(),
|
||||
GamepadButton::face_b(),
|
||||
GamepadButton::face_y(),
|
||||
GamepadButton::lshoulder(),
|
||||
GamepadButton::rshoulder(),
|
||||
ButtonHandle::none(),
|
||||
ButtonHandle::none(),
|
||||
GamepadButton::back(),
|
||||
GamepadButton::start(),
|
||||
};
|
||||
gamepad_buttons = gamepad_buttons_snes;
|
||||
}
|
||||
ButtonRegistry *registry = ButtonRegistry::ptr();
|
||||
|
||||
// Prepare a mapping of data indices to button/axis indices.
|
||||
_indices.resize(caps.NumberInputDataIndices);
|
||||
@ -414,7 +303,7 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
|
||||
}
|
||||
}
|
||||
|
||||
nassertd(cap.Range.DataIndexMin + upper < _indices.size()) continue;
|
||||
nassertd(cap.Range.DataIndexMin + upper < (int)_indices.size()) continue;
|
||||
|
||||
// Windows will only tell us which buttons in a report are "on", so we
|
||||
// need to keep track of which buttons exist in which report so that we
|
||||
@ -429,8 +318,10 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
|
||||
switch (cap.UsagePage) {
|
||||
case HID_USAGE_PAGE_BUTTON:
|
||||
if (_device_class == DeviceClass::gamepad) {
|
||||
if (usage < sizeof(gamepad_buttons_common) / sizeof(ButtonHandle)) {
|
||||
handle = gamepad_buttons[usage];
|
||||
if (usage > 0 && usage - 1 < _countof(default_gamepad_mapping)) {
|
||||
if (gamepad_buttons[usage - 1] != nullptr) {
|
||||
handle = registry->find_button(gamepad_buttons[usage - 1]);
|
||||
}
|
||||
}
|
||||
} else if (_device_class == DeviceClass::flight_stick) {
|
||||
if (usage > 0) {
|
||||
@ -491,7 +382,7 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
|
||||
}
|
||||
}
|
||||
|
||||
nassertd(cap.Range.DataIndexMin + upper < _indices.size()) continue;
|
||||
nassertd(cap.Range.DataIndexMin + upper < (int)_indices.size()) continue;
|
||||
|
||||
for (int j = 0; j <= upper; ++j) {
|
||||
USAGE usage = j + cap.Range.UsageMin;
|
||||
@ -530,9 +421,17 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
|
||||
break;
|
||||
case HID_USAGE_GENERIC_Z:
|
||||
if (_device_class == DeviceClass::gamepad) {
|
||||
if ((quirks & QB_no_analog_triggers) == 0) {
|
||||
axis = Axis::left_trigger;
|
||||
}
|
||||
} else if (_device_class == DeviceClass::flight_stick) {
|
||||
axis = Axis::throttle;
|
||||
if ((quirks & QB_reversed_throttle) != 0) {
|
||||
std::swap(cap.LogicalMin, cap.LogicalMax);
|
||||
}
|
||||
if ((quirks & QB_centered_throttle) != 0) {
|
||||
is_signed = false;
|
||||
}
|
||||
} else {
|
||||
axis = Axis::z;
|
||||
swap(cap.LogicalMin, cap.LogicalMax);
|
||||
@ -555,7 +454,9 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
|
||||
break;
|
||||
case HID_USAGE_GENERIC_RZ:
|
||||
if (_device_class == DeviceClass::gamepad) {
|
||||
if ((quirks & QB_no_analog_triggers) == 0) {
|
||||
axis = Axis::right_trigger;
|
||||
}
|
||||
} else {
|
||||
// Flip to match Panda's convention for heading.
|
||||
axis = Axis::yaw;
|
||||
@ -580,10 +481,7 @@ on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
|
||||
}
|
||||
|
||||
int axis_index;
|
||||
if (_vendor_id == 0x044f && _product_id == 0xb108 && axis == Axis::throttle) {
|
||||
// T.Flight Hotas X throttle is reversed and can go backwards.
|
||||
axis_index = add_axis(axis, cap.LogicalMax, cap.LogicalMin, true);
|
||||
} else if (!is_signed) {
|
||||
if (!is_signed) {
|
||||
// All axes on the weird XInput-style mappings go from -1 to 1
|
||||
axis_index = add_axis(axis, cap.LogicalMin, cap.LogicalMax, true);
|
||||
} else {
|
||||
|
@ -99,12 +99,12 @@ typedef struct _XINPUT_CAPABILITIES_EX {
|
||||
WORD Unknown2;
|
||||
} XINPUT_CAPABILITIES_EX;
|
||||
|
||||
typedef DWORD (*pXInputGetState)(DWORD, XINPUT_STATE *);
|
||||
typedef DWORD (*pXInputSetState)(DWORD, XINPUT_VIBRATION *);
|
||||
typedef DWORD (*pXInputGetCapabilities)(DWORD, DWORD, XINPUT_CAPABILITIES *);
|
||||
typedef DWORD (*pXInputGetCapabilitiesEx)(DWORD, DWORD, DWORD, XINPUT_CAPABILITIES_EX *);
|
||||
typedef DWORD (*pXInputGetBatteryInformation)(DWORD, BYTE, XINPUT_BATTERY_INFORMATION *);
|
||||
typedef DWORD (*pXInputGetBaseBusInformation)(DWORD, XINPUT_BUSINFO *);
|
||||
typedef DWORD (WINAPI *pXInputGetState)(DWORD, XINPUT_STATE *);
|
||||
typedef DWORD (WINAPI *pXInputSetState)(DWORD, XINPUT_VIBRATION *);
|
||||
typedef DWORD (WINAPI *pXInputGetCapabilities)(DWORD, DWORD, XINPUT_CAPABILITIES *);
|
||||
typedef DWORD (WINAPI *pXInputGetCapabilitiesEx)(DWORD, DWORD, DWORD, XINPUT_CAPABILITIES_EX *);
|
||||
typedef DWORD (WINAPI *pXInputGetBatteryInformation)(DWORD, BYTE, XINPUT_BATTERY_INFORMATION *);
|
||||
typedef DWORD (WINAPI *pXInputGetBaseBusInformation)(DWORD, XINPUT_BUSINFO *);
|
||||
|
||||
static pXInputGetState get_state = nullptr;
|
||||
static pXInputSetState set_state = nullptr;
|
||||
|
@ -97,6 +97,14 @@ has_tangent() const {
|
||||
return (_flags & F_has_tangent) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
INLINE bool EggVertexUV::
|
||||
has_tangent4() const {
|
||||
return (_flags & F_has_tangent4) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@ -106,6 +114,19 @@ get_tangent() const {
|
||||
return _tangent;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
INLINE LVecBase4d EggVertexUV::
|
||||
get_tangent4() const {
|
||||
LVecBase4d tangent4(_tangent, 1.0);
|
||||
nassertr_always(has_tangent(), tangent4);
|
||||
if (_flags & F_flip_computed_binormal) {
|
||||
tangent4[3] = -1.0;
|
||||
}
|
||||
return tangent4;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@ -113,6 +134,22 @@ INLINE void EggVertexUV::
|
||||
set_tangent(const LNormald &tangent) {
|
||||
_tangent = tangent;
|
||||
_flags |= F_has_tangent;
|
||||
_flags &= ~(F_has_tangent4 | F_flip_computed_binormal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tangent vector, along with a fourth parameter that is multiplied
|
||||
* with the result of cross(normal, tangent) when computing the binormal.
|
||||
*/
|
||||
INLINE void EggVertexUV::
|
||||
set_tangent4(const LVecBase4d &tangent) {
|
||||
_tangent = tangent.get_xyz();
|
||||
_flags |= F_has_tangent4 | F_has_tangent;
|
||||
if (tangent[3] < 0.0) {
|
||||
_flags |= F_flip_computed_binormal;
|
||||
} else {
|
||||
_flags &= ~F_flip_computed_binormal;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -145,7 +145,10 @@ write(std::ostream &out, int indent_level) const {
|
||||
} else {
|
||||
indent(out, indent_level+2) << get_uv() << "\n";
|
||||
}
|
||||
if (has_tangent()) {
|
||||
if (has_tangent4()) {
|
||||
indent(out, indent_level + 2)
|
||||
<< "<Tangent> { " << get_tangent4() << " }\n";
|
||||
} else if (has_tangent()) {
|
||||
indent(out, indent_level + 2)
|
||||
<< "<Tangent> { " << get_tangent() << " }\n";
|
||||
}
|
||||
|
@ -45,8 +45,11 @@ PUBLISHED:
|
||||
INLINE void set_uvw(const LTexCoord3d &texCoord);
|
||||
|
||||
INLINE bool has_tangent() const;
|
||||
INLINE bool has_tangent4() const;
|
||||
INLINE const LNormald &get_tangent() const;
|
||||
INLINE LVecBase4d get_tangent4() const;
|
||||
INLINE void set_tangent(const LNormald &tangent);
|
||||
INLINE void set_tangent4(const LVecBase4d &tangent);
|
||||
INLINE void clear_tangent();
|
||||
|
||||
INLINE bool has_binormal() const;
|
||||
@ -69,6 +72,10 @@ private:
|
||||
F_has_tangent = 0x001,
|
||||
F_has_binormal = 0x002,
|
||||
F_has_w = 0x004,
|
||||
F_has_tangent4 = 0x008,
|
||||
|
||||
// Only defined temporarily as we can't add a float to this class in 1.10.
|
||||
F_flip_computed_binormal = 0x010,
|
||||
};
|
||||
|
||||
int _flags;
|
||||
|
@ -1103,6 +1103,14 @@ vertex_uv_body:
|
||||
} else {
|
||||
DCAST(EggVertexUV, egg_stack.back())->set_tangent(LNormald($4, $5, $6));
|
||||
}
|
||||
}
|
||||
| vertex_uv_body TANGENT '{' real real real real '}'
|
||||
{
|
||||
if (DCAST(EggVertexUV, egg_stack.back())->has_tangent()) {
|
||||
eggyywarning("Ignoring repeated tangent");
|
||||
} else {
|
||||
DCAST(EggVertexUV, egg_stack.back())->set_tangent4(LVecBase4d($4, $5, $6, $7));
|
||||
}
|
||||
}
|
||||
| vertex_uv_body BINORMAL '{' real real real '}'
|
||||
{
|
||||
|
84
tests/egg/test_egg_vertex_uv.py
Normal file
84
tests/egg/test_egg_vertex_uv.py
Normal file
@ -0,0 +1,84 @@
|
||||
import pytest
|
||||
from panda3d import core
|
||||
|
||||
# Skip these tests if we can't import egg.
|
||||
egg = pytest.importorskip("panda3d.egg")
|
||||
|
||||
|
||||
def read_egg_vertex(string):
|
||||
"""Reads an EggVertex from a string."""
|
||||
data = "<VertexPool> pool { <Vertex> 1 { %s } }" % (string)
|
||||
stream = core.StringStream(data.encode('utf-8'))
|
||||
data = egg.EggData()
|
||||
assert data.read(stream)
|
||||
pool, = data.get_children()
|
||||
return pool.get_vertex(1)
|
||||
|
||||
|
||||
def test_egg_vertex_uv_empty():
|
||||
vertex = read_egg_vertex("""
|
||||
0 0 0
|
||||
<UV> {
|
||||
0 0
|
||||
}
|
||||
""")
|
||||
|
||||
obj = vertex.get_uv_obj("")
|
||||
assert not obj.has_tangent()
|
||||
assert not obj.has_tangent4()
|
||||
|
||||
assert '<Tangent>' not in str(obj)
|
||||
|
||||
|
||||
def test_egg_vertex_tangent():
|
||||
vertex = read_egg_vertex("""
|
||||
0 0 0
|
||||
<UV> {
|
||||
0 0
|
||||
<Tangent> { 2 3 4 }
|
||||
}
|
||||
""")
|
||||
|
||||
obj = vertex.get_uv_obj("")
|
||||
assert obj.has_tangent()
|
||||
assert not obj.has_tangent4()
|
||||
assert obj.get_tangent() == (2, 3, 4)
|
||||
assert obj.get_tangent4() == (2, 3, 4, 1)
|
||||
|
||||
assert '{ 2 3 4 }' in str(obj)
|
||||
|
||||
|
||||
def test_egg_vertex_tangent4_pos():
|
||||
vertex = read_egg_vertex("""
|
||||
0 0 0
|
||||
<UV> {
|
||||
0 0
|
||||
<Tangent> { 2 3 4 1 }
|
||||
}
|
||||
""")
|
||||
|
||||
obj = vertex.get_uv_obj("")
|
||||
assert obj.has_tangent()
|
||||
assert obj.has_tangent4()
|
||||
assert obj.get_tangent() == (2, 3, 4)
|
||||
assert obj.get_tangent4() == (2, 3, 4, 1)
|
||||
|
||||
assert '{ 2 3 4 1 }' in str(obj)
|
||||
|
||||
|
||||
def test_egg_vertex_tangent4_neg():
|
||||
vertex = read_egg_vertex("""
|
||||
0 0 0
|
||||
<UV> {
|
||||
0 0
|
||||
<Tangent> { 2 3 4 -1 }
|
||||
}
|
||||
""")
|
||||
|
||||
obj = vertex.get_uv_obj("")
|
||||
assert obj.has_tangent()
|
||||
assert obj.has_tangent4()
|
||||
assert obj.get_tangent() == (2, 3, 4)
|
||||
assert obj.get_tangent4() == (2, 3, 4, -1)
|
||||
|
||||
assert '{ 2 3 4 -1 }' in str(obj)
|
Loading…
x
Reference in New Issue
Block a user