1353 lines
38 KiB
C++
1353 lines
38 KiB
C++
/**
|
|
* 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 pgItem.cxx
|
|
* @author drose
|
|
* @date 2002-03-13
|
|
*/
|
|
|
|
#include "pgItem.h"
|
|
#include "pgMouseWatcherParameter.h"
|
|
#include "pgCullTraverser.h"
|
|
#include "config_pgui.h"
|
|
#include "boundingVolume.h"
|
|
#include "pandaNode.h"
|
|
#include "sceneGraphReducer.h"
|
|
#include "throw_event.h"
|
|
#include "string_utils.h"
|
|
#include "nodePath.h"
|
|
#include "cullTraverser.h"
|
|
#include "cullTraverserData.h"
|
|
#include "cullBinManager.h"
|
|
#include "clipPlaneAttrib.h"
|
|
#include "scissorAttrib.h"
|
|
#include "dcast.h"
|
|
#include "boundingSphere.h"
|
|
#include "boundingBox.h"
|
|
#include "config_mathutil.h"
|
|
#include "pipeline.h"
|
|
|
|
#ifdef HAVE_AUDIO
|
|
#include "audioSound.h"
|
|
#endif
|
|
|
|
using std::string;
|
|
|
|
TypeHandle PGItem::_type_handle;
|
|
PT(TextNode) PGItem::_text_node;
|
|
PGItem *PGItem::_focus_item = nullptr;
|
|
PGItem::BackgroundFocus PGItem::_background_focus;
|
|
|
|
|
|
/**
|
|
* Returns true if the 2-d v1 is to the right of v2.
|
|
*/
|
|
INLINE bool
|
|
is_right(const LVector2 &v1, const LVector2 &v2) {
|
|
return (v1[0] * v2[1] - v1[1] * v2[0]) > 0;
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
PGItem::
|
|
PGItem(const string &name) :
|
|
PandaNode(name),
|
|
_lock(name),
|
|
_notify(nullptr),
|
|
_has_frame(false),
|
|
_frame(0, 0, 0, 0),
|
|
_state(0),
|
|
_flags(0),
|
|
_region(new PGMouseWatcherRegion(this))
|
|
{
|
|
set_cull_callback();
|
|
set_renderable();
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
PGItem::
|
|
~PGItem() {
|
|
if (_notify != nullptr) {
|
|
_notify->remove_item(this);
|
|
_notify = nullptr;
|
|
}
|
|
|
|
nassertv(_region->_item == this);
|
|
_region->_item = nullptr;
|
|
|
|
set_background_focus(false);
|
|
if (_focus_item == this) {
|
|
_focus_item = nullptr;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
PGItem::
|
|
PGItem(const PGItem ©) :
|
|
PandaNode(copy),
|
|
_notify(nullptr),
|
|
_has_frame(copy._has_frame),
|
|
_frame(copy._frame),
|
|
_state(copy._state),
|
|
_flags(copy._flags),
|
|
_region(new PGMouseWatcherRegion(this))
|
|
#ifdef HAVE_AUDIO
|
|
, _sounds(copy._sounds)
|
|
#endif
|
|
{
|
|
// We give our region the same name as the region for the PGItem we're
|
|
// copying--so that this PGItem will generate the same event names when the
|
|
// user interacts with it.
|
|
_region->set_name(copy._region->get_name());
|
|
|
|
// Make a deep copy of all of the original PGItem's StateDefs.
|
|
size_t num_state_defs = copy._state_defs.size();
|
|
_state_defs.reserve(num_state_defs);
|
|
for (size_t i = 0; i < num_state_defs; ++i) {
|
|
// We cheat and cast away the const, because the frame is just a cache.
|
|
// But we have to get the frame out of the source before we can safely
|
|
// copy it.
|
|
StateDef &old_sd = (StateDef &)(copy._state_defs[i]);
|
|
old_sd._frame.remove_node();
|
|
old_sd._frame_stale = true;
|
|
|
|
StateDef new_sd;
|
|
new_sd._root = old_sd._root.copy_to(NodePath());
|
|
new_sd._frame_style = old_sd._frame_style;
|
|
|
|
_state_defs.push_back(new_sd);
|
|
|
|
#ifdef THREADED_PIPELINE
|
|
if (Pipeline::get_render_pipeline()->get_num_stages() > 1) {
|
|
((PGItem &)copy).update_frame((int)i);
|
|
update_frame((int)i);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a newly-allocated Node that is a shallow copy of this one. It will
|
|
* be a different Node pointer, but its internal data may or may not be shared
|
|
* with that of the original Node.
|
|
*/
|
|
PandaNode *PGItem::
|
|
make_copy() const {
|
|
LightReMutexHolder holder(_lock);
|
|
return new PGItem(*this);
|
|
}
|
|
|
|
/**
|
|
* Called after the node's transform has been changed for any reason, this
|
|
* just provides a hook so derived classes can do something special in this
|
|
* case.
|
|
*/
|
|
void PGItem::
|
|
transform_changed() {
|
|
LightReMutexHolder holder(_lock);
|
|
PandaNode::transform_changed();
|
|
if (_notify != nullptr) {
|
|
_notify->item_transform_changed(this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called after the node's draw_mask has been changed for any reason, this
|
|
* just provides a hook so derived classes can do something special in this
|
|
* case.
|
|
*/
|
|
void PGItem::
|
|
draw_mask_changed() {
|
|
LightReMutexHolder holder(_lock);
|
|
PandaNode::draw_mask_changed();
|
|
if (_notify != nullptr) {
|
|
_notify->item_draw_mask_changed(this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This function will be called during the cull traversal to perform any
|
|
* additional operations that should be performed at cull time. This may
|
|
* include additional manipulation of render state or additional
|
|
* visible/invisible decisions, or any other arbitrary operation.
|
|
*
|
|
* Note that this function will *not* be called unless set_cull_callback() is
|
|
* called in the constructor of the derived class. It is necessary to call
|
|
* set_cull_callback() to indicated that we require cull_callback() to be
|
|
* called.
|
|
*
|
|
* By the time this function is called, the node has already passed the
|
|
* bounding-volume test for the viewing frustum, and the node's transform and
|
|
* state have already been applied to the indicated CullTraverserData object.
|
|
*
|
|
* The return value is true if this node should be visible, or false if it
|
|
* should be culled.
|
|
*/
|
|
bool PGItem::
|
|
cull_callback(CullTraverser *trav, CullTraverserData &data) {
|
|
CullTraverser::_pgui_nodes_pcollector.add_level(1);
|
|
|
|
// We try not to hold the lock for longer than necessary.
|
|
PT(PandaNode) state_def_root;
|
|
bool has_frame;
|
|
PGMouseWatcherRegion *region;
|
|
{
|
|
LightReMutexHolder holder(_lock);
|
|
has_frame = _has_frame && ((_flags & F_active) != 0);
|
|
region = _region;
|
|
|
|
int state = _state;
|
|
if (state >= 0 && (size_t)state < _state_defs.size()) {
|
|
StateDef &state_def = _state_defs[state];
|
|
if (!state_def._root.is_empty()) {
|
|
if (Thread::get_current_pipeline_stage() == 0) {
|
|
if (state_def._frame_stale) {
|
|
update_frame(state);
|
|
}
|
|
}
|
|
|
|
state_def_root = state_def._root.node();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (has_frame && !data.is_this_node_hidden(trav->get_camera_mask())) {
|
|
// The item has a frame, so we want to generate a region for it and update
|
|
// the MouseWatcher.
|
|
|
|
// We can only do this if our traverser is a PGCullTraverser (which will
|
|
// be the case if this node was parented somewhere under a PGTop node).
|
|
if (trav->is_exact_type(PGCullTraverser::get_class_type())) {
|
|
PGCullTraverser *pg_trav;
|
|
DCAST_INTO_R(pg_trav, trav, true);
|
|
|
|
const LMatrix4 &transform = data.get_net_transform(trav)->get_mat();
|
|
|
|
// Consider the cull bin this object is in. Since the binning affects
|
|
// the render order, we want bins that render later to get higher sort
|
|
// values.
|
|
int bin_index = data._state->get_bin_index();
|
|
int sort;
|
|
|
|
CullBinManager *bin_manager = CullBinManager::get_global_ptr();
|
|
CullBinManager::BinType bin_type = bin_manager->get_bin_type(bin_index);
|
|
if (bin_type == CullBinManager::BT_fixed) {
|
|
// If the bin is a "fixed" type bin, our local sort is based on the
|
|
// fixed order.
|
|
sort = data._state->get_draw_order();
|
|
|
|
} else if (bin_type == CullBinManager::BT_unsorted) {
|
|
// If the bin is an "unsorted" type bin, we base the local sort on the
|
|
// scene graph order.
|
|
sort = pg_trav->_sort_index;
|
|
pg_trav->_sort_index++;
|
|
|
|
} else {
|
|
// Otherwise, the local sort is irrelevant.
|
|
sort = 0;
|
|
}
|
|
|
|
// Now what order does this bin sort relative to the other bins? This
|
|
// becomes the high-order part of the final sort count.
|
|
int bin_sort = bin_manager->get_bin_sort(data._state->get_bin_index());
|
|
|
|
// Combine the two sorts into a single int. This assumes we only need
|
|
// 16 bits for each sort number, possibly an erroneous assumption. We
|
|
// should really provide two separate sort values, both ints, in the
|
|
// MouseWatcherRegion; but in the interest of expediency we work within
|
|
// the existing interface which only provides one.
|
|
sort = (bin_sort << 16) | ((sort + 0x8000) & 0xffff);
|
|
|
|
const ClipPlaneAttrib *clip = nullptr;
|
|
const ScissorAttrib *scissor = nullptr;
|
|
data._state->get_attrib(clip);
|
|
data._state->get_attrib(scissor);
|
|
if (activate_region(transform, sort, clip, scissor)) {
|
|
pg_trav->_top->add_region(region);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (state_def_root != nullptr) {
|
|
// This item has a current state definition that we should use to render
|
|
// the item.
|
|
trav->traverse_down(data, state_def_root);
|
|
}
|
|
|
|
// Now continue to render everything else below this node.
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Called when needed to recompute the node's _internal_bound object. Nodes
|
|
* that contain anything of substance should redefine this to do the right
|
|
* thing.
|
|
*/
|
|
void PGItem::
|
|
compute_internal_bounds(CPT(BoundingVolume) &internal_bounds,
|
|
int &internal_vertices,
|
|
int pipeline_stage,
|
|
Thread *current_thread) const {
|
|
LightReMutexHolder holder(_lock, current_thread);
|
|
int num_vertices = 0;
|
|
|
|
// First, get ourselves a fresh, empty bounding volume.
|
|
PT(BoundingVolume) bound;
|
|
|
|
BoundingVolume::BoundsType btype = get_bounds_type();
|
|
if (btype == BoundingVolume::BT_default) {
|
|
btype = bounds_type;
|
|
}
|
|
|
|
if (btype == BoundingVolume::BT_sphere) {
|
|
bound = new BoundingSphere;
|
|
} else {
|
|
bound = new BoundingBox;
|
|
}
|
|
|
|
// Now actually compute the bounding volume by putting it around all of our
|
|
// states' bounding volumes.
|
|
pvector<const BoundingVolume *> child_volumes;
|
|
|
|
// We walk through the list of state defs indirectly, calling
|
|
// get_state_def() on each one, to ensure that the frames are updated
|
|
// correctly before we measure their bounding volumes.
|
|
for (int i = 0; i < (int)_state_defs.size(); i++) {
|
|
NodePath &root = ((PGItem *)this)->do_get_state_def(i);
|
|
if (!root.is_empty()) {
|
|
PandaNode *node = root.node();
|
|
child_volumes.push_back(node->get_bounds(current_thread));
|
|
num_vertices += node->get_nested_vertices(current_thread);
|
|
}
|
|
}
|
|
|
|
const BoundingVolume **child_begin = &child_volumes[0];
|
|
const BoundingVolume **child_end = child_begin + child_volumes.size();
|
|
|
|
bound->around(child_begin, child_end);
|
|
|
|
internal_bounds = bound;
|
|
internal_vertices = num_vertices;
|
|
}
|
|
|
|
/**
|
|
* The recursive implementation of prepare_scene(). Don't call this directly;
|
|
* call PandaNode::prepare_scene() or NodePath::prepare_scene() instead.
|
|
*/
|
|
void PGItem::
|
|
r_prepare_scene(GraphicsStateGuardianBase *gsg, const RenderState *node_state,
|
|
GeomTransformer &transformer, Thread *current_thread) {
|
|
LightReMutexHolder holder(_lock);
|
|
for (StateDef &def : _state_defs) {
|
|
NodePath &root = def._root;
|
|
if (!root.is_empty()) {
|
|
PandaNode *child = root.node();
|
|
CPT(RenderState) child_state = node_state->compose(child->get_state());
|
|
child->r_prepare_scene(gsg, child_state, transformer, current_thread);
|
|
}
|
|
}
|
|
|
|
PandaNode::r_prepare_scene(gsg, node_state, transformer, current_thread);
|
|
}
|
|
|
|
/**
|
|
* Transforms the contents of this node by the indicated matrix, if it means
|
|
* anything to do so. For most kinds of nodes, this does nothing.
|
|
*/
|
|
void PGItem::
|
|
xform(const LMatrix4 &mat) {
|
|
LightReMutexHolder holder(_lock);
|
|
// Transform the frame.
|
|
LPoint3 ll(_frame[0], 0.0f, _frame[2]);
|
|
LPoint3 ur(_frame[1], 0.0f, _frame[3]);
|
|
ll = ll * mat;
|
|
ur = ur * mat;
|
|
_frame.set(ll[0], ur[0], ll[2], ur[2]);
|
|
|
|
// Transform the individual states and their frame styles.
|
|
for (size_t state = 0; state < _state_defs.size(); ++state) {
|
|
StateDef &def = _state_defs[state];
|
|
NodePath &root = def._root;
|
|
// Apply the matrix to the previous transform.
|
|
root.set_transform(root.get_transform()->compose(TransformState::make_mat(mat)));
|
|
|
|
// Now flatten the transform into the subgraph.
|
|
SceneGraphReducer gr;
|
|
gr.apply_attribs(root.node());
|
|
|
|
// Transform the frame style too.
|
|
if (def._frame_style.xform(mat)) {
|
|
#ifdef THREADED_PIPELINE
|
|
if (Pipeline::get_render_pipeline()->get_num_stages() > 1) {
|
|
update_frame((int)state);
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
def._frame_stale = true;
|
|
}
|
|
}
|
|
}
|
|
mark_internal_bounds_stale();
|
|
}
|
|
|
|
/**
|
|
* Applies the indicated scene graph transform and order as determined by the
|
|
* traversal from PGTop.
|
|
*
|
|
* The return value is true if the region is valid, or false if it is empty or
|
|
* completely clipped.
|
|
*/
|
|
bool PGItem::
|
|
activate_region(const LMatrix4 &transform, int sort,
|
|
const ClipPlaneAttrib *cpa,
|
|
const ScissorAttrib *sa) {
|
|
using std::min;
|
|
using std::max;
|
|
|
|
LightReMutexHolder holder(_lock);
|
|
// Transform all four vertices, and get the new bounding box. This way the
|
|
// region works (mostly) even if has been rotated.
|
|
LPoint3 ll = LPoint3::rfu(_frame[0], 0.0f, _frame[2]) * transform;
|
|
LPoint3 lr = LPoint3::rfu(_frame[1], 0.0f, _frame[2]) * transform;
|
|
LPoint3 ul = LPoint3::rfu(_frame[0], 0.0f, _frame[3]) * transform;
|
|
LPoint3 ur = LPoint3::rfu(_frame[1], 0.0f, _frame[3]) * transform;
|
|
LVector3 up = LVector3::up();
|
|
int up_axis;
|
|
if (up[1]) {
|
|
up_axis = 1;
|
|
}
|
|
else if (up[2]) {
|
|
up_axis = 2;
|
|
}
|
|
else {
|
|
up_axis = 0;
|
|
}
|
|
LVector3 right = LVector3::right();
|
|
int right_axis;
|
|
if (right[0]) {
|
|
right_axis = 0;
|
|
}
|
|
else if (right[2]) {
|
|
right_axis = 2;
|
|
}
|
|
else {
|
|
right_axis = 1;
|
|
}
|
|
|
|
LVecBase4 frame;
|
|
if (cpa != nullptr && cpa->get_num_on_planes() != 0) {
|
|
// Apply the clip plane(s) andor scissor region now that we are here in
|
|
// world space.
|
|
|
|
ClipPoints points;
|
|
points.reserve(4);
|
|
points.push_back(LPoint2(ll[right_axis], ll[up_axis]));
|
|
points.push_back(LPoint2(lr[right_axis], lr[up_axis]));
|
|
points.push_back(LPoint2(ur[right_axis], ur[up_axis]));
|
|
points.push_back(LPoint2(ul[right_axis], ul[up_axis]));
|
|
|
|
int num_on_planes = cpa->get_num_on_planes();
|
|
for (int i = 0; i < num_on_planes; ++i) {
|
|
NodePath plane_path = cpa->get_on_plane(i);
|
|
LPlane plane = DCAST(PlaneNode, plane_path.node())->get_plane();
|
|
plane.xform(plane_path.get_net_transform()->get_mat());
|
|
|
|
// We ignore the forward axis, assuming the frame is still in the right-
|
|
// up plane after being transformed. Not sure if we really need to
|
|
// support general 3-D transforms on 2-D objects.
|
|
clip_frame(points, plane);
|
|
}
|
|
|
|
if (points.empty()) {
|
|
// Turns out it's completely clipped after all.
|
|
return false;
|
|
}
|
|
|
|
ClipPoints::iterator pi;
|
|
pi = points.begin();
|
|
frame.set((*pi)[0], (*pi)[0], (*pi)[1], (*pi)[1]);
|
|
++pi;
|
|
while (pi != points.end()) {
|
|
frame[0] = min(frame[0], (*pi)[0]);
|
|
frame[1] = max(frame[1], (*pi)[0]);
|
|
frame[2] = min(frame[2], (*pi)[1]);
|
|
frame[3] = max(frame[3], (*pi)[1]);
|
|
++pi;
|
|
}
|
|
} else {
|
|
// Since there are no clip planes involved, just set the frame.
|
|
frame.set(min(min(ll[right_axis], lr[right_axis]), min(ul[right_axis], ur[right_axis])),
|
|
max(max(ll[right_axis], lr[right_axis]), max(ul[right_axis], ur[right_axis])),
|
|
min(min(ll[up_axis], lr[up_axis]), min(ul[up_axis], ur[up_axis])),
|
|
max(max(ll[up_axis], lr[up_axis]), max(ul[up_axis], ur[up_axis])));
|
|
}
|
|
|
|
if (sa != nullptr) {
|
|
// Also restrict it to within the scissor region.
|
|
const LVecBase4 &sf = sa->get_frame();
|
|
// Expand sf from 0..1 to -1..1.
|
|
frame.set(max(frame[0], sf[0] * 2.0f - 1.0f),
|
|
min(frame[1], sf[1] * 2.0f - 1.0f),
|
|
max(frame[2], sf[2] * 2.0f - 1.0f),
|
|
min(frame[3], sf[3] * 2.0f - 1.0f));
|
|
if (frame[1] <= frame[0] || frame[3] <= frame[2]) {
|
|
// Completely outside the scissor region.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
_region->set_frame(frame);
|
|
|
|
_region->set_sort(sort);
|
|
_region->set_active(true);
|
|
|
|
// calculate the inverse of this transform, which is needed to go back to
|
|
// the frame space.
|
|
_frame_inv_xform.invert_from(transform);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* This is a callback hook function, called whenever the mouse enters the
|
|
* region. The mouse is only considered to be "entered" in one region at a
|
|
* time; in the case of nested regions, it exits the outer region before
|
|
* entering the inner one.
|
|
*/
|
|
void PGItem::
|
|
enter_region(const MouseWatcherParameter ¶m) {
|
|
LightReMutexHolder holder(_lock);
|
|
if (pgui_cat.is_debug()) {
|
|
pgui_cat.debug()
|
|
<< *this << "::enter_region(" << param << ")\n";
|
|
}
|
|
|
|
PGMouseWatcherParameter *ep = new PGMouseWatcherParameter(param);
|
|
string event = get_enter_event();
|
|
play_sound(event);
|
|
throw_event(event, EventParameter(ep));
|
|
|
|
if (_notify != nullptr) {
|
|
_notify->item_enter(this, param);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is a callback hook function, called whenever the mouse exits the
|
|
* region. The mouse is only considered to be "entered" in one region at a
|
|
* time; in the case of nested regions, it exits the outer region before
|
|
* entering the inner one.
|
|
*/
|
|
void PGItem::
|
|
exit_region(const MouseWatcherParameter ¶m) {
|
|
LightReMutexHolder holder(_lock);
|
|
if (pgui_cat.is_debug()) {
|
|
pgui_cat.debug()
|
|
<< *this << "::exit_region(" << param << ")\n";
|
|
}
|
|
|
|
PGMouseWatcherParameter *ep = new PGMouseWatcherParameter(param);
|
|
string event = get_exit_event();
|
|
play_sound(event);
|
|
throw_event(event, EventParameter(ep));
|
|
|
|
if (_notify != nullptr) {
|
|
_notify->item_exit(this, param);
|
|
}
|
|
|
|
// pgui_cat.info() << get_name() << "::exit()" << endl;
|
|
}
|
|
|
|
/**
|
|
* This is a callback hook function, called whenever the mouse moves within
|
|
* the boundaries of the region, even if it is also within the boundaries of a
|
|
* nested region. This is different from "enter", which is only called
|
|
* whenever the mouse is within only that region.
|
|
*/
|
|
void PGItem::
|
|
within_region(const MouseWatcherParameter ¶m) {
|
|
LightReMutexHolder holder(_lock);
|
|
if (pgui_cat.is_debug()) {
|
|
pgui_cat.debug()
|
|
<< *this << "::within_region(" << param << ")\n";
|
|
}
|
|
|
|
PGMouseWatcherParameter *ep = new PGMouseWatcherParameter(param);
|
|
string event = get_within_event();
|
|
play_sound(event);
|
|
throw_event(event, EventParameter(ep));
|
|
|
|
if (_notify != nullptr) {
|
|
_notify->item_within(this, param);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is a callback hook function, called whenever the mouse moves
|
|
* completely outside the boundaries of the region. See within().
|
|
*/
|
|
void PGItem::
|
|
without_region(const MouseWatcherParameter ¶m) {
|
|
LightReMutexHolder holder(_lock);
|
|
if (pgui_cat.is_debug()) {
|
|
pgui_cat.debug()
|
|
<< *this << "::without_region(" << param << ")\n";
|
|
}
|
|
|
|
PGMouseWatcherParameter *ep = new PGMouseWatcherParameter(param);
|
|
string event = get_without_event();
|
|
play_sound(event);
|
|
throw_event(event, EventParameter(ep));
|
|
|
|
if (_notify != nullptr) {
|
|
_notify->item_without(this, param);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is a callback hook function, called whenever the widget gets the
|
|
* keyboard focus.
|
|
*/
|
|
void PGItem::
|
|
focus_in() {
|
|
LightReMutexHolder holder(_lock);
|
|
if (pgui_cat.is_debug()) {
|
|
pgui_cat.debug()
|
|
<< *this << "::focus_in()\n";
|
|
}
|
|
|
|
string event = get_focus_in_event();
|
|
play_sound(event);
|
|
throw_event(event);
|
|
|
|
if (_notify != nullptr) {
|
|
_notify->item_focus_in(this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is a callback hook function, called whenever the widget loses the
|
|
* keyboard focus.
|
|
*/
|
|
void PGItem::
|
|
focus_out() {
|
|
LightReMutexHolder holder(_lock);
|
|
if (pgui_cat.is_debug()) {
|
|
pgui_cat.debug()
|
|
<< *this << "::focus_out()\n";
|
|
}
|
|
|
|
string event = get_focus_out_event();
|
|
play_sound(event);
|
|
throw_event(event);
|
|
|
|
if (_notify != nullptr) {
|
|
_notify->item_focus_out(this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is a callback hook function, called whenever a mouse or keyboard
|
|
* button is depressed while the mouse is within the region.
|
|
*/
|
|
void PGItem::
|
|
press(const MouseWatcherParameter ¶m, bool background) {
|
|
LightReMutexHolder holder(_lock);
|
|
if (pgui_cat.is_debug()) {
|
|
pgui_cat.debug()
|
|
<< *this << "::press(" << param << ", " << background << ")\n";
|
|
}
|
|
|
|
if (!background) {
|
|
PGMouseWatcherParameter *ep = new PGMouseWatcherParameter(param);
|
|
string event;
|
|
if (param.is_keyrepeat()) {
|
|
event = get_repeat_event(param.get_button());
|
|
} else {
|
|
event = get_press_event(param.get_button());
|
|
}
|
|
play_sound(event);
|
|
throw_event(event, EventParameter(ep));
|
|
}
|
|
|
|
if (_notify != nullptr) {
|
|
_notify->item_press(this, param);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is a callback hook function, called whenever a mouse or keyboard
|
|
* button previously depressed with press() is released.
|
|
*/
|
|
void PGItem::
|
|
release(const MouseWatcherParameter ¶m, bool background) {
|
|
LightReMutexHolder holder(_lock);
|
|
if (pgui_cat.is_debug()) {
|
|
pgui_cat.debug()
|
|
<< *this << "::release(" << param << ", " << background << ")\n";
|
|
}
|
|
|
|
if (!background) {
|
|
PGMouseWatcherParameter *ep = new PGMouseWatcherParameter(param);
|
|
string event = get_release_event(param.get_button());
|
|
play_sound(event);
|
|
throw_event(event, EventParameter(ep));
|
|
}
|
|
|
|
if (_notify != nullptr) {
|
|
_notify->item_release(this, param);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is a callback hook function, called whenever the user presses a key.
|
|
*/
|
|
void PGItem::
|
|
keystroke(const MouseWatcherParameter ¶m, bool background) {
|
|
LightReMutexHolder holder(_lock);
|
|
if (pgui_cat.is_debug()) {
|
|
pgui_cat.debug()
|
|
<< *this << "::keystroke(" << param << ", " << background << ")\n";
|
|
}
|
|
|
|
if (!background) {
|
|
PGMouseWatcherParameter *ep = new PGMouseWatcherParameter(param);
|
|
string event = get_keystroke_event();
|
|
play_sound(event);
|
|
throw_event(event, EventParameter(ep));
|
|
|
|
if (has_notify()) {
|
|
get_notify()->item_keystroke(this, param);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is a callback hook function, called whenever the user highlights an
|
|
* option in the IME window.
|
|
*/
|
|
void PGItem::
|
|
candidate(const MouseWatcherParameter ¶m, bool background) {
|
|
LightReMutexHolder holder(_lock);
|
|
if (pgui_cat.is_debug()) {
|
|
pgui_cat.debug()
|
|
<< *this << "::candidate(" << param << ", " << background << ")\n";
|
|
}
|
|
|
|
// We don't throw sound events for candidate selections for now.
|
|
if (!background) {
|
|
if (has_notify()) {
|
|
get_notify()->item_candidate(this, param);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is a callback hook function, called whenever a mouse is moved while
|
|
* within the region.
|
|
*/
|
|
void PGItem::
|
|
move(const MouseWatcherParameter ¶m) {
|
|
LightReMutexHolder holder(_lock);
|
|
if (pgui_cat.is_debug()) {
|
|
pgui_cat.debug()
|
|
<< *this << "::move(" << param << ")\n";
|
|
}
|
|
|
|
if (_notify != nullptr) {
|
|
_notify->item_move(this, param);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calls press() on all the PGItems with background focus.
|
|
*/
|
|
void PGItem::
|
|
background_press(const MouseWatcherParameter ¶m) {
|
|
for (PGItem *item : _background_focus) {
|
|
if (!item->get_focus()) {
|
|
item->press(param, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calls release() on all the PGItems with background focus.
|
|
*/
|
|
void PGItem::
|
|
background_release(const MouseWatcherParameter ¶m) {
|
|
for (PGItem *item : _background_focus) {
|
|
if (!item->get_focus()) {
|
|
item->release(param, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calls keystroke() on all the PGItems with background focus.
|
|
*/
|
|
void PGItem::
|
|
background_keystroke(const MouseWatcherParameter ¶m) {
|
|
for (PGItem *item : _background_focus) {
|
|
if (!item->get_focus()) {
|
|
item->keystroke(param, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calls candidate() on all the PGItems with background focus.
|
|
*/
|
|
void PGItem::
|
|
background_candidate(const MouseWatcherParameter ¶m) {
|
|
for (PGItem *item : _background_focus) {
|
|
if (!item->get_focus()) {
|
|
item->candidate(param, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets whether the PGItem is active for mouse watching. This is not
|
|
* necessarily related to the active/inactive appearance of the item, which is
|
|
* controlled by set_state(), but it does affect whether it responds to mouse
|
|
* events.
|
|
*/
|
|
void PGItem::
|
|
set_active(bool active) {
|
|
LightReMutexHolder holder(_lock);
|
|
if (active) {
|
|
_flags |= F_active;
|
|
} else {
|
|
_flags &= ~F_active;
|
|
// Deactivating the item automatically defocuses it too.
|
|
if (get_focus()) {
|
|
set_focus(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets whether the PGItem currently has keyboard focus. This simply means
|
|
* that the item may respond to keyboard events as well as to mouse events;
|
|
* precisely what this means is up to the individual item.
|
|
*
|
|
* Only one PGItem in the world is allowed to have focus at any given time.
|
|
* Setting the focus on any other item automatically disables the focus from
|
|
* the previous item.
|
|
*/
|
|
void PGItem::
|
|
set_focus(bool focus) {
|
|
LightReMutexHolder holder(_lock);
|
|
if (focus) {
|
|
if (!get_active()) {
|
|
// Cannot set focus on an inactive item.
|
|
return;
|
|
}
|
|
|
|
// Set the keyboard focus to this item.
|
|
if (_focus_item != this) {
|
|
if (_focus_item != nullptr) {
|
|
// Clear the focus from whatever item currently has it.
|
|
_focus_item->set_focus(false);
|
|
}
|
|
_focus_item = this;
|
|
}
|
|
if (!get_focus()) {
|
|
focus_in();
|
|
_flags |= F_focus;
|
|
}
|
|
|
|
} else {
|
|
if (_focus_item == this) {
|
|
// Remove this item from the focus.
|
|
_focus_item = nullptr;
|
|
}
|
|
|
|
if (get_focus()) {
|
|
focus_out();
|
|
_flags &= ~F_focus;
|
|
}
|
|
}
|
|
_region->set_keyboard(focus);
|
|
}
|
|
|
|
/**
|
|
* Sets the background_focus flag for this item. When background_focus is
|
|
* enabled, the item will receive keypress events even if it is not in focus;
|
|
* in fact, even if it is not onscreen. Unlike normal focus, many items may
|
|
* have background_focus simultaneously.
|
|
*/
|
|
void PGItem::
|
|
set_background_focus(bool focus) {
|
|
LightReMutexHolder holder(_lock);
|
|
if (focus != get_background_focus()) {
|
|
if (focus) {
|
|
// Activate background focus.
|
|
_flags |= F_background_focus;
|
|
bool inserted = _background_focus.insert(this).second;
|
|
nassertv(inserted);
|
|
|
|
} else {
|
|
// Deactivate background focus.
|
|
_flags &= ~F_background_focus;
|
|
size_t num_erased = _background_focus.erase(this);
|
|
nassertv(num_erased == 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns one more than the highest-numbered state def that was ever assigned
|
|
* to the PGItem. The complete set of state defs assigned may then be
|
|
* retrieved by indexing from 0 to (get_num_state_defs() - 1).
|
|
*
|
|
* This is only an upper limit on the actual number of state defs, since there
|
|
* may be holes in the list.
|
|
*/
|
|
int PGItem::
|
|
get_num_state_defs() const {
|
|
LightReMutexHolder holder(_lock);
|
|
return _state_defs.size();
|
|
}
|
|
|
|
/**
|
|
* Returns true if get_state_def() has ever been called for the indicated
|
|
* state (thus defining a render subgraph for this state index), false
|
|
* otherwise.
|
|
*/
|
|
bool PGItem::
|
|
has_state_def(int state) const {
|
|
LightReMutexHolder holder(_lock);
|
|
if (state < 0 || state >= (int)_state_defs.size()) {
|
|
return false;
|
|
}
|
|
return (!_state_defs[state]._root.is_empty());
|
|
}
|
|
|
|
/**
|
|
* Resets the NodePath assigned to the indicated state to its initial default,
|
|
* with only a frame representation if appropriate.
|
|
*/
|
|
void PGItem::
|
|
clear_state_def(int state) {
|
|
LightReMutexHolder holder(_lock);
|
|
if (state < 0 || state >= (int)_state_defs.size()) {
|
|
return;
|
|
}
|
|
|
|
_state_defs[state]._root = NodePath();
|
|
_state_defs[state]._frame = NodePath();
|
|
_state_defs[state]._frame_stale = true;
|
|
|
|
mark_internal_bounds_stale();
|
|
|
|
#ifdef THREADED_PIPELINE
|
|
if (Pipeline::get_render_pipeline()->get_num_stages() > 1) {
|
|
update_frame(state);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* Parents an instance of the bottom node of the indicated NodePath to the
|
|
* indicated state index.
|
|
*/
|
|
NodePath PGItem::
|
|
instance_to_state_def(int state, const NodePath &path) {
|
|
LightReMutexHolder holder(_lock);
|
|
if (path.is_empty()) {
|
|
// If the source is empty, quietly do nothing.
|
|
return NodePath();
|
|
}
|
|
|
|
mark_internal_bounds_stale();
|
|
|
|
return path.instance_to(do_get_state_def(state));
|
|
}
|
|
|
|
/**
|
|
* Returns the kind of frame that will be drawn behind the item when it is in
|
|
* the indicated state.
|
|
*/
|
|
PGFrameStyle PGItem::
|
|
get_frame_style(int state) {
|
|
LightReMutexHolder holder(_lock);
|
|
if (state < 0 || state >= (int)_state_defs.size()) {
|
|
return PGFrameStyle();
|
|
}
|
|
return _state_defs[state]._frame_style;
|
|
}
|
|
|
|
/**
|
|
* Changes the kind of frame that will be drawn behind the item when it is in
|
|
* the indicated state.
|
|
*/
|
|
void PGItem::
|
|
set_frame_style(int state, const PGFrameStyle &style) {
|
|
LightReMutexHolder holder(_lock);
|
|
|
|
slot_state_def(state);
|
|
|
|
if (_state_defs[state]._root.is_empty()) {
|
|
// Create a new node.
|
|
_state_defs[state]._root = NodePath("state_" + format_string(state));
|
|
}
|
|
|
|
_state_defs[state]._frame_style = style;
|
|
_state_defs[state]._frame_stale = true;
|
|
|
|
mark_internal_bounds_stale();
|
|
update_frame(state);
|
|
}
|
|
|
|
#ifdef HAVE_AUDIO
|
|
/**
|
|
* Sets the sound that will be played whenever the indicated event occurs.
|
|
*/
|
|
void PGItem::
|
|
set_sound(const string &event, AudioSound *sound) {
|
|
LightReMutexHolder holder(_lock);
|
|
_sounds[event] = sound;
|
|
}
|
|
|
|
/**
|
|
* Removes the sound associated with the indicated event.
|
|
*/
|
|
void PGItem::
|
|
clear_sound(const string &event) {
|
|
LightReMutexHolder holder(_lock);
|
|
_sounds.erase(event);
|
|
}
|
|
|
|
/**
|
|
* Returns the sound associated with the indicated event, or NULL if there is
|
|
* no associated sound.
|
|
*/
|
|
AudioSound *PGItem::
|
|
get_sound(const string &event) const {
|
|
LightReMutexHolder holder(_lock);
|
|
Sounds::const_iterator si = _sounds.find(event);
|
|
if (si != _sounds.end()) {
|
|
return (*si).second;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
/**
|
|
* Returns true if there is a sound associated with the indicated event, or
|
|
* false otherwise.
|
|
*/
|
|
bool PGItem::
|
|
has_sound(const string &event) const {
|
|
LightReMutexHolder holder(_lock);
|
|
return (_sounds.count(event) != 0);
|
|
}
|
|
#endif // HAVE_AUDIO
|
|
|
|
/**
|
|
* Returns the TextNode object that will be used by all PGItems to generate
|
|
* default labels given a string. This can be loaded with the default font,
|
|
* etc.
|
|
*/
|
|
TextNode *PGItem::
|
|
get_text_node() {
|
|
if (_text_node == nullptr) {
|
|
_text_node = new TextNode("pguiText");
|
|
_text_node->set_text_color(0.0f, 0.0f, 0.0f, 1.0f);
|
|
|
|
// The default TextNode is aligned to the left, for the convenience of
|
|
// PGEntry.
|
|
_text_node->set_align(TextNode::A_left);
|
|
}
|
|
return _text_node;
|
|
}
|
|
|
|
/**
|
|
* Plays the sound associated with the indicated event, if there is one.
|
|
*/
|
|
void PGItem::
|
|
play_sound(const string &event) {
|
|
#ifdef HAVE_AUDIO
|
|
LightReMutexHolder holder(_lock);
|
|
Sounds::const_iterator si = _sounds.find(event);
|
|
if (si != _sounds.end()) {
|
|
AudioSound *sound = (*si).second;
|
|
sound->play();
|
|
}
|
|
#endif // HAVE_AUDIO
|
|
}
|
|
|
|
/**
|
|
* The frame parameter is an in/out parameter. This function adjusts frame so
|
|
* that it represents the largest part of the rectangular region passed in,
|
|
* that does not overlap with the rectangular region of the indicated
|
|
* obscurer. If the obscurer is NULL, or is a hidden node, it is not
|
|
* considered and the frame is left unchanged.
|
|
*
|
|
* This is used by slider bars and scroll frames, which have to automatically
|
|
* figure out how much space they have to work with after allowing space for
|
|
* scroll bars and buttons.
|
|
*/
|
|
void PGItem::
|
|
reduce_region(LVecBase4 &frame, PGItem *obscurer) const {
|
|
using std::min;
|
|
using std::max;
|
|
|
|
if (obscurer != nullptr && !obscurer->is_overall_hidden()) {
|
|
LVecBase4 oframe = get_relative_frame(obscurer);
|
|
|
|
// Determine the four rectangular regions on the four sides of the
|
|
// obscuring region.
|
|
LVecBase4 right(max(frame[0], oframe[1]), frame[1], frame[2], frame[3]);
|
|
LVecBase4 left(frame[0], min(frame[1], oframe[0]), frame[2], frame[3]);
|
|
LVecBase4 above(frame[0], frame[1], max(frame[2], oframe[3]), frame[3]);
|
|
LVecBase4 below(frame[0], frame[1], frame[2], min(frame[3], oframe[2]));
|
|
|
|
// Now choose the largest of those four.
|
|
const LVecBase4 *largest = &right;
|
|
PN_stdfloat largest_area = compute_area(*largest);
|
|
compare_largest(largest, largest_area, &left);
|
|
compare_largest(largest, largest_area, &above);
|
|
compare_largest(largest, largest_area, &below);
|
|
|
|
frame = *largest;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the LVecBase4 frame of the indicated item, converted into this
|
|
* item's coordinate space. Presumably, item is a child of this node.
|
|
*/
|
|
LVecBase4 PGItem::
|
|
get_relative_frame(PGItem *item) const {
|
|
using std::min;
|
|
using std::max;
|
|
|
|
NodePath this_np = NodePath::any_path((PGItem *)this);
|
|
NodePath item_np = this_np.find_path_to(item);
|
|
if (item_np.is_empty()) {
|
|
item_np = NodePath::any_path(item);
|
|
}
|
|
const LVecBase4 &orig_frame = item->get_frame();
|
|
LMatrix4 transform = item_np.get_mat(this_np);
|
|
|
|
// Transform the item's frame into the PGScrollFrame's coordinate space.
|
|
// Transform all four vertices, and get the new bounding box. This way the
|
|
// region works (mostly) even if has been rotated.
|
|
LPoint3 ll(orig_frame[0], 0.0f, orig_frame[2]);
|
|
LPoint3 lr(orig_frame[1], 0.0f, orig_frame[2]);
|
|
LPoint3 ul(orig_frame[0], 0.0f, orig_frame[3]);
|
|
LPoint3 ur(orig_frame[1], 0.0f, orig_frame[3]);
|
|
ll = ll * transform;
|
|
lr = lr * transform;
|
|
ul = ul * transform;
|
|
ur = ur * transform;
|
|
|
|
return LVecBase4(min(min(ll[0], lr[0]), min(ul[0], ur[0])),
|
|
max(max(ll[0], lr[0]), max(ul[0], ur[0])),
|
|
min(min(ll[2], lr[2]), min(ul[2], ur[2])),
|
|
max(max(ll[2], lr[2]), max(ul[2], ur[2])));
|
|
}
|
|
|
|
/**
|
|
* Converts from the 2-d mouse coordinates into the coordinate space of the
|
|
* item.
|
|
*/
|
|
LPoint3 PGItem::
|
|
mouse_to_local(const LPoint2 &mouse_point) const {
|
|
// This is ambiguous if the PGItem has multiple instances. Why would you do
|
|
// that, anyway?
|
|
NodePath this_np((PGItem *)this);
|
|
CPT(TransformState) inv_transform = NodePath().get_transform(this_np);
|
|
return inv_transform->get_mat().xform_point(LVector3::rfu(mouse_point[0], 0, mouse_point[1]));
|
|
}
|
|
|
|
/**
|
|
* Called when the user changes the frame size. Assumes the lock is held.
|
|
*/
|
|
void PGItem::
|
|
frame_changed() {
|
|
mark_frames_stale();
|
|
if (_notify != nullptr) {
|
|
_notify->item_frame_changed(this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the Node that is the root of the subgraph that will be drawn when
|
|
* the PGItem is in the indicated state. The first time this is called for a
|
|
* particular state index, it may create the Node.
|
|
*
|
|
* Assumes the lock is already held.
|
|
*/
|
|
NodePath &PGItem::
|
|
do_get_state_def(int state) {
|
|
slot_state_def(state);
|
|
|
|
if (_state_defs[state]._root.is_empty()) {
|
|
// Create a new node.
|
|
_state_defs[state]._root = NodePath("state_" + format_string(state));
|
|
_state_defs[state]._frame_stale = true;
|
|
}
|
|
|
|
if (_state_defs[state]._frame_stale) {
|
|
update_frame(state);
|
|
}
|
|
|
|
return _state_defs[state]._root;
|
|
}
|
|
|
|
/**
|
|
* Ensures there is a slot in the array for the given state definition.
|
|
* Assumes the lock is already held.
|
|
*/
|
|
void PGItem::
|
|
slot_state_def(int state) {
|
|
while (state >= (int)_state_defs.size()) {
|
|
_state_defs.push_back(StateDef());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates a new instance of the frame geometry for the indicated state.
|
|
* Assumes the lock is already held.
|
|
*/
|
|
void PGItem::
|
|
update_frame(int state) {
|
|
// First, remove the old frame geometry, if any.
|
|
if (state >= 0 && state < (int)_state_defs.size()) {
|
|
_state_defs[state]._frame.remove_node();
|
|
}
|
|
|
|
// We must turn off the stale flag first, before we call get_state_def(), to
|
|
// prevent get_state_def() from being a recursive call.
|
|
_state_defs[state]._frame_stale = false;
|
|
|
|
// Now create new frame geometry.
|
|
if (has_frame()) {
|
|
NodePath &root = do_get_state_def(state);
|
|
_state_defs[state]._frame =
|
|
_state_defs[state]._frame_style.generate_into(root, _frame);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Marks all the frames in all states stale, so that they will be regenerated
|
|
* the next time each state is requested. Assumes the lock is already held.
|
|
*/
|
|
void PGItem::
|
|
mark_frames_stale() {
|
|
#ifdef THREADED_PIPELINE
|
|
// If we are using the threaded pipeline, we must update the frame geometry
|
|
// immediately on the App thread, since this class isn't pipeline-cycled.
|
|
if (Pipeline::get_render_pipeline()->get_num_stages() > 1) {
|
|
for (int state = 0; state < (int)_state_defs.size(); ++state) {
|
|
update_frame(state);
|
|
}
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
for (StateDef &def : _state_defs) {
|
|
// Remove the old frame, if any.
|
|
def._frame.remove_node();
|
|
def._frame_stale = true;
|
|
}
|
|
}
|
|
mark_internal_bounds_stale();
|
|
}
|
|
|
|
/**
|
|
* Clips the four corners of the item's frame by the indicated clipping plane,
|
|
* and modifies the points to reflect the new set of clipped points.
|
|
*
|
|
* The return value is true if the set of points is unmodified (all points are
|
|
* behind the clip plane), or false otherwise.
|
|
*/
|
|
bool PGItem::
|
|
clip_frame(ClipPoints &source_points, const LPlane &plane) const {
|
|
if (source_points.empty()) {
|
|
return true;
|
|
}
|
|
|
|
LPoint3 from3d;
|
|
LVector3 delta3d;
|
|
if (!plane.intersects_plane(from3d, delta3d, LPlane(LVector3(0, 1, 0), LPoint3::zero()))) {
|
|
// The clipping plane is parallel to the polygon. The polygon is either
|
|
// all in or all out.
|
|
if (plane.dist_to_plane(LPoint3::zero()) < 0.0) {
|
|
// A point within the polygon is behind the clipping plane: the polygon
|
|
// is all in.
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Project the line of intersection into the X-Z plane. Now we have a 2-d
|
|
// clipping line.
|
|
LPoint2 from2d(from3d[0], from3d[2]);
|
|
LVector2 delta2d(delta3d[0], delta3d[2]);
|
|
|
|
PN_stdfloat a = -delta2d[1];
|
|
PN_stdfloat b = delta2d[0];
|
|
PN_stdfloat c = from2d[0] * delta2d[1] - from2d[1] * delta2d[0];
|
|
|
|
// Now walk through the points. Any point on the left of our line gets
|
|
// removed, and the line segment clipped at the point of intersection.
|
|
|
|
// We might increase the number of vertices by as many as 1, if the plane
|
|
// clips off exactly one corner. (We might also decrease the number of
|
|
// vertices, or keep them the same number.)
|
|
ClipPoints new_points;
|
|
new_points.reserve(source_points.size() + 1);
|
|
|
|
LPoint2 last_point(source_points.back());
|
|
bool last_is_in = is_right(last_point - from2d, delta2d);
|
|
bool all_in = last_is_in;
|
|
|
|
for (LPoint2 this_point : source_points) {
|
|
bool this_is_in = is_right(this_point - from2d, delta2d);
|
|
|
|
// There appears to be a compiler bug in gcc 4.0: we need to extract this
|
|
// comparison outside of the if statement.
|
|
bool crossed_over = (this_is_in != last_is_in);
|
|
if (crossed_over) {
|
|
// We have just crossed over the clipping line. Find the point of
|
|
// intersection.
|
|
LVector2 d = this_point - last_point;
|
|
PN_stdfloat denom = (a * d[0] + b * d[1]);
|
|
if (denom != 0.0) {
|
|
PN_stdfloat t = -(a * last_point[0] + b * last_point[1] + c) / denom;
|
|
LPoint2 p = last_point + t * d;
|
|
|
|
new_points.push_back(p);
|
|
last_is_in = this_is_in;
|
|
}
|
|
}
|
|
|
|
if (this_is_in) {
|
|
// We are behind the clipping line. Keep the point.
|
|
new_points.push_back(this_point);
|
|
} else {
|
|
all_in = false;
|
|
}
|
|
|
|
last_point = this_point;
|
|
}
|
|
|
|
source_points.swap(new_points);
|
|
return all_in;
|
|
}
|