mirror of
https://github.com/beestat/app.git
synced 2026-02-26 05:00:21 -05:00
Openings
This commit is contained in:
parent
1c714aa05b
commit
42be5e4d7d
@ -82,6 +82,9 @@ beestat.component.card.floor_plan_editor.prototype.decorate_contents_ = function
|
||||
if (group.trees === undefined) {
|
||||
group.trees = [];
|
||||
}
|
||||
if (group.openings === undefined) {
|
||||
group.openings = [];
|
||||
}
|
||||
|
||||
group.rooms.forEach(function(room) {
|
||||
if (room.room_id === undefined) {
|
||||
@ -121,6 +124,28 @@ beestat.component.card.floor_plan_editor.prototype.decorate_contents_ = function
|
||||
tree.editor_locked = false;
|
||||
}
|
||||
});
|
||||
|
||||
group.openings.forEach(function(opening) {
|
||||
if (opening.opening_id === undefined) {
|
||||
opening.opening_id = window.crypto.randomUUID();
|
||||
}
|
||||
if (opening.editor_hidden === undefined) {
|
||||
opening.editor_hidden = opening.editor_visible === false;
|
||||
}
|
||||
delete opening.editor_visible;
|
||||
if (opening.editor_locked === undefined) {
|
||||
opening.editor_locked = false;
|
||||
}
|
||||
if (['empty', 'door', 'window'].includes(opening.type) !== true) {
|
||||
opening.type = 'empty';
|
||||
}
|
||||
if (opening.width === undefined) {
|
||||
opening.width = 36;
|
||||
}
|
||||
if (opening.height === undefined) {
|
||||
opening.height = 80;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
@ -417,6 +442,14 @@ beestat.component.card.floor_plan_editor.prototype.decorate_drawing_pane_ = func
|
||||
self.update_floor_plan_();
|
||||
self.rerender();
|
||||
});
|
||||
this.floor_plan_.addEventListener('add_opening', function() {
|
||||
self.update_floor_plan_();
|
||||
self.rerender();
|
||||
});
|
||||
this.floor_plan_.addEventListener('remove_opening', function() {
|
||||
self.update_floor_plan_();
|
||||
self.rerender();
|
||||
});
|
||||
this.floor_plan_.addEventListener('undo', function() {
|
||||
self.update_floor_plan_();
|
||||
self.rerender();
|
||||
@ -430,7 +463,8 @@ beestat.component.card.floor_plan_editor.prototype.decorate_drawing_pane_ = func
|
||||
this.entity_index_ = {
|
||||
'rooms': {},
|
||||
'surfaces': {},
|
||||
'trees': {}
|
||||
'trees': {},
|
||||
'openings': {}
|
||||
};
|
||||
|
||||
const on_entity_update = function() {
|
||||
@ -484,6 +518,17 @@ beestat.component.card.floor_plan_editor.prototype.decorate_drawing_pane_ = func
|
||||
.set_group(self.state_.active_group);
|
||||
surface_entity.render(self.floor_plan_.get_g());
|
||||
});
|
||||
(group_below.openings || []).slice().reverse().forEach(function(opening) {
|
||||
if (opening.editor_hidden === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const opening_entity = new beestat.component.floor_plan_entity.opening(self.floor_plan_, self.state_)
|
||||
.set_enabled(false)
|
||||
.set_opening(opening)
|
||||
.set_group(self.state_.active_group);
|
||||
opening_entity.render(self.floor_plan_.get_g());
|
||||
});
|
||||
}
|
||||
|
||||
// Loop over the rooms in this group and add them.
|
||||
@ -571,6 +616,42 @@ beestat.component.card.floor_plan_editor.prototype.decorate_drawing_pane_ = func
|
||||
this.state_.active_surface_entity.render(this.floor_plan_.get_g());
|
||||
}
|
||||
|
||||
// Loop over openings in this group and add them.
|
||||
let active_opening_entity;
|
||||
(this.state_.active_group.openings || []).slice().reverse().forEach(function(opening) {
|
||||
if (opening.editor_hidden === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const opening_entity = new beestat.component.floor_plan_entity.opening(self.floor_plan_, self.state_)
|
||||
.set_enabled(opening.editor_locked !== true)
|
||||
.set_opening(opening)
|
||||
.set_group(self.state_.active_group);
|
||||
|
||||
opening_entity.addEventListener('update', on_entity_update);
|
||||
opening_entity.addEventListener('activate', on_entity_activate);
|
||||
opening_entity.addEventListener('inactivate', on_entity_inactivate);
|
||||
|
||||
if (
|
||||
self.state_.active_opening_entity !== undefined &&
|
||||
opening.opening_id === self.state_.active_opening_entity.get_opening().opening_id
|
||||
) {
|
||||
delete self.state_.active_opening_entity;
|
||||
active_opening_entity = opening_entity;
|
||||
}
|
||||
|
||||
opening_entity.render(self.floor_plan_.get_g());
|
||||
self.entity_index_.openings[opening.opening_id] = opening_entity;
|
||||
});
|
||||
|
||||
if (active_opening_entity !== undefined) {
|
||||
active_opening_entity.set_active(true);
|
||||
}
|
||||
|
||||
if (this.state_.active_opening_entity !== undefined) {
|
||||
this.state_.active_opening_entity.render(this.floor_plan_.get_g());
|
||||
}
|
||||
|
||||
// Trees are only editable on the first floor.
|
||||
const tree_group = this.floor_plan_.get_tree_group_();
|
||||
if (tree_group === this.state_.active_group) {
|
||||
@ -632,11 +713,16 @@ beestat.component.card.floor_plan_editor.prototype.update_layers_sidebar_ = func
|
||||
* Select an object from the layers sidebar.
|
||||
*
|
||||
* @param {object} group
|
||||
* @param {string} type rooms|surfaces|trees
|
||||
* @param {string} type rooms|surfaces|openings|trees
|
||||
* @param {string} object_id
|
||||
*/
|
||||
beestat.component.card.floor_plan_editor.prototype.select_layer_object_ = function(group, type, object_id) {
|
||||
const normalized_type = type === 'room' ? 'rooms' : type;
|
||||
let normalized_type = type;
|
||||
if (normalized_type === 'room') {
|
||||
normalized_type = 'rooms';
|
||||
} else if (normalized_type === 'opening') {
|
||||
normalized_type = 'openings';
|
||||
}
|
||||
const object = this.get_layer_object_by_id_(group, normalized_type, object_id);
|
||||
const is_active_group = (
|
||||
this.state_.active_group !== undefined &&
|
||||
@ -670,7 +756,7 @@ beestat.component.card.floor_plan_editor.prototype.select_layer_object_ = functi
|
||||
* Set an object's editor visibility.
|
||||
*
|
||||
* @param {object} group
|
||||
* @param {string} type rooms|surfaces|trees
|
||||
* @param {string} type rooms|surfaces|openings|trees
|
||||
* @param {string} object_id
|
||||
* @param {boolean} visible
|
||||
*/
|
||||
@ -703,6 +789,13 @@ beestat.component.card.floor_plan_editor.prototype.set_layer_object_visibility_
|
||||
) {
|
||||
this.state_.active_tree_entity.set_active(false);
|
||||
}
|
||||
if (
|
||||
type === 'openings' &&
|
||||
this.state_.active_opening_entity !== undefined &&
|
||||
this.state_.active_opening_entity.get_opening().opening_id === object_id
|
||||
) {
|
||||
this.state_.active_opening_entity.set_active(false);
|
||||
}
|
||||
}
|
||||
|
||||
this.floor_plan_.update_infobox();
|
||||
@ -716,7 +809,7 @@ beestat.component.card.floor_plan_editor.prototype.set_layer_object_visibility_
|
||||
* Set an object's editor lock.
|
||||
*
|
||||
* @param {object} group
|
||||
* @param {string} type rooms|surfaces|trees
|
||||
* @param {string} type rooms|surfaces|openings|trees
|
||||
* @param {string} object_id
|
||||
* @param {boolean} locked
|
||||
*/
|
||||
@ -750,6 +843,13 @@ beestat.component.card.floor_plan_editor.prototype.set_layer_object_locked_ = fu
|
||||
) {
|
||||
this.state_.active_tree_entity.set_active(false);
|
||||
}
|
||||
if (
|
||||
type === 'openings' &&
|
||||
this.state_.active_opening_entity !== undefined &&
|
||||
this.state_.active_opening_entity.get_opening().opening_id === object_id
|
||||
) {
|
||||
this.state_.active_opening_entity.set_active(false);
|
||||
}
|
||||
}
|
||||
|
||||
this.floor_plan_.update_infobox();
|
||||
@ -763,7 +863,7 @@ beestat.component.card.floor_plan_editor.prototype.set_layer_object_locked_ = fu
|
||||
* Lock or unlock all objects in a layer type.
|
||||
*
|
||||
* @param {object} group
|
||||
* @param {string} type rooms|surfaces|trees
|
||||
* @param {string} type rooms|surfaces|openings|trees
|
||||
* @param {boolean} locked
|
||||
*/
|
||||
beestat.component.card.floor_plan_editor.prototype.set_layer_locked_ = function(group, type, locked) {
|
||||
@ -783,7 +883,7 @@ beestat.component.card.floor_plan_editor.prototype.set_layer_locked_ = function(
|
||||
* Hide or show all objects in a type layer.
|
||||
*
|
||||
* @param {object} group
|
||||
* @param {string} type rooms|surfaces|trees
|
||||
* @param {string} type rooms|surfaces|openings|trees
|
||||
* @param {boolean} visible
|
||||
*/
|
||||
beestat.component.card.floor_plan_editor.prototype.set_layer_visible_ = function(group, type, visible) {
|
||||
@ -806,7 +906,7 @@ beestat.component.card.floor_plan_editor.prototype.set_layer_visible_ = function
|
||||
* @param {boolean} locked
|
||||
*/
|
||||
beestat.component.card.floor_plan_editor.prototype.set_group_locked_ = function(group, locked) {
|
||||
['rooms', 'surfaces', 'trees'].forEach(function(type) {
|
||||
['rooms', 'surfaces', 'openings', 'trees'].forEach(function(type) {
|
||||
const collection = group[type] || [];
|
||||
collection.forEach(function(object) {
|
||||
object.editor_locked = locked;
|
||||
@ -816,6 +916,7 @@ beestat.component.card.floor_plan_editor.prototype.set_group_locked_ = function(
|
||||
if (locked === true) {
|
||||
this.deactivate_active_entity_for_group_type_(group, 'rooms');
|
||||
this.deactivate_active_entity_for_group_type_(group, 'surfaces');
|
||||
this.deactivate_active_entity_for_group_type_(group, 'openings');
|
||||
this.deactivate_active_entity_for_group_type_(group, 'trees');
|
||||
}
|
||||
|
||||
@ -829,7 +930,7 @@ beestat.component.card.floor_plan_editor.prototype.set_group_locked_ = function(
|
||||
* @param {boolean} visible
|
||||
*/
|
||||
beestat.component.card.floor_plan_editor.prototype.set_group_visible_ = function(group, visible) {
|
||||
['rooms', 'surfaces', 'trees'].forEach(function(type) {
|
||||
['rooms', 'surfaces', 'openings', 'trees'].forEach(function(type) {
|
||||
const collection = group[type] || [];
|
||||
collection.forEach(function(object) {
|
||||
object.editor_hidden = visible !== true;
|
||||
@ -839,6 +940,7 @@ beestat.component.card.floor_plan_editor.prototype.set_group_visible_ = function
|
||||
if (visible !== true) {
|
||||
this.deactivate_active_entity_for_group_type_(group, 'rooms');
|
||||
this.deactivate_active_entity_for_group_type_(group, 'surfaces');
|
||||
this.deactivate_active_entity_for_group_type_(group, 'openings');
|
||||
this.deactivate_active_entity_for_group_type_(group, 'trees');
|
||||
}
|
||||
|
||||
@ -849,7 +951,7 @@ beestat.component.card.floor_plan_editor.prototype.set_group_visible_ = function
|
||||
* Deactivate active entity if it belongs to the specified group/type.
|
||||
*
|
||||
* @param {object} group
|
||||
* @param {string} type rooms|surfaces|trees
|
||||
* @param {string} type rooms|surfaces|openings|trees
|
||||
*/
|
||||
beestat.component.card.floor_plan_editor.prototype.deactivate_active_entity_for_group_type_ = function(group, type) {
|
||||
if (type === 'rooms' && this.state_.active_room_entity !== undefined) {
|
||||
@ -870,6 +972,13 @@ beestat.component.card.floor_plan_editor.prototype.deactivate_active_entity_for_
|
||||
if (this.state_.active_tree_entity.group_ === group) {
|
||||
this.state_.active_tree_entity.set_active(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'openings' && this.state_.active_opening_entity !== undefined) {
|
||||
if (this.state_.active_opening_entity.group_ === group) {
|
||||
this.state_.active_opening_entity.set_active(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -957,6 +1066,16 @@ beestat.component.card.floor_plan_editor.prototype.ensure_active_entity_visibili
|
||||
) {
|
||||
delete this.state_.active_tree_entity;
|
||||
}
|
||||
|
||||
if (
|
||||
this.state_.active_opening_entity !== undefined &&
|
||||
(
|
||||
this.state_.active_opening_entity.get_opening().editor_hidden === true ||
|
||||
this.state_.active_opening_entity.get_opening().editor_locked === true
|
||||
)
|
||||
) {
|
||||
delete this.state_.active_opening_entity;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -1003,6 +1122,9 @@ beestat.component.card.floor_plan_editor.prototype.get_layer_object_id_key_ = fu
|
||||
if (type === 'surfaces') {
|
||||
return 'surface_id';
|
||||
}
|
||||
if (type === 'openings') {
|
||||
return 'opening_id';
|
||||
}
|
||||
return 'tree_id';
|
||||
};
|
||||
|
||||
@ -1010,7 +1132,7 @@ beestat.component.card.floor_plan_editor.prototype.get_layer_object_id_key_ = fu
|
||||
* Get object by id from a group/type.
|
||||
*
|
||||
* @param {object} group
|
||||
* @param {string} type rooms|surfaces|trees
|
||||
* @param {string} type rooms|surfaces|openings|trees
|
||||
* @param {string} object_id
|
||||
*
|
||||
* @return {object|undefined}
|
||||
@ -1036,6 +1158,8 @@ beestat.component.card.floor_plan_editor.prototype.expand_layers_for_active_enti
|
||||
let type;
|
||||
if (this.state_.active_tree_entity !== undefined) {
|
||||
type = 'trees';
|
||||
} else if (this.state_.active_opening_entity !== undefined) {
|
||||
type = 'openings';
|
||||
} else if (this.state_.active_surface_entity !== undefined) {
|
||||
type = 'surfaces';
|
||||
} else if (this.state_.active_room_entity !== undefined) {
|
||||
@ -1063,6 +1187,9 @@ beestat.component.card.floor_plan_editor.prototype.scroll_layers_to_active_entit
|
||||
if (this.state_.active_tree_entity !== undefined) {
|
||||
type = 'trees';
|
||||
object_id = this.state_.active_tree_entity.get_tree().tree_id;
|
||||
} else if (this.state_.active_opening_entity !== undefined) {
|
||||
type = 'openings';
|
||||
object_id = this.state_.active_opening_entity.get_opening().opening_id;
|
||||
} else if (this.state_.active_surface_entity !== undefined) {
|
||||
type = 'surfaces';
|
||||
object_id = this.state_.active_surface_entity.get_surface().surface_id;
|
||||
@ -1117,6 +1244,12 @@ beestat.component.card.floor_plan_editor.prototype.restore_entity_draw_order_ =
|
||||
'surface_id'
|
||||
);
|
||||
|
||||
append_entities_in_order(
|
||||
this.state_.active_group.openings || [],
|
||||
this.entity_index_.openings || {},
|
||||
'opening_id'
|
||||
);
|
||||
|
||||
const tree_group = this.floor_plan_.get_tree_group_();
|
||||
if (tree_group === this.state_.active_group) {
|
||||
append_entities_in_order(
|
||||
@ -1135,6 +1268,8 @@ beestat.component.card.floor_plan_editor.prototype.restore_entity_draw_order_ =
|
||||
beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_ = function(parent) {
|
||||
if (this.state_.active_tree_entity !== undefined) {
|
||||
this.decorate_info_pane_tree_(parent);
|
||||
} else if (this.state_.active_opening_entity !== undefined) {
|
||||
this.decorate_info_pane_opening_(parent);
|
||||
} else if (this.state_.active_surface_entity !== undefined) {
|
||||
this.decorate_info_pane_surface_(parent);
|
||||
} else if (this.state_.active_room_entity !== undefined) {
|
||||
@ -1643,6 +1778,155 @@ beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_surface_ =
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Decorate the info pane for an opening.
|
||||
*
|
||||
* @param {rocket.Elements} parent
|
||||
*/
|
||||
beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_opening_ = function(parent) {
|
||||
const self = this;
|
||||
const opening = this.state_.active_opening_entity.get_opening();
|
||||
|
||||
const grid = $.createElement('div')
|
||||
.style({
|
||||
'display': 'grid',
|
||||
'grid-template-columns': 'repeat(auto-fit, minmax(150px, 1fr))',
|
||||
'column-gap': beestat.style.size.gutter
|
||||
});
|
||||
parent.appendChild(grid);
|
||||
|
||||
let div;
|
||||
|
||||
// Name
|
||||
div = $.createElement('div');
|
||||
grid.appendChild(div);
|
||||
const name_input = new beestat.component.input.text()
|
||||
.set_label('Opening Name')
|
||||
.set_placeholder('Unnamed Opening')
|
||||
.set_width('100%')
|
||||
.set_maxlength(50)
|
||||
.render(div);
|
||||
|
||||
if (opening.name !== undefined) {
|
||||
name_input.set_value(opening.name);
|
||||
}
|
||||
|
||||
name_input.addEventListener('input', function() {
|
||||
opening.name = name_input.get_value();
|
||||
self.update_layers_sidebar_();
|
||||
});
|
||||
name_input.addEventListener('change', function() {
|
||||
opening.name = name_input.get_value();
|
||||
self.update_floor_plan_();
|
||||
self.update_layers_sidebar_();
|
||||
});
|
||||
|
||||
// Type
|
||||
div = $.createElement('div');
|
||||
grid.appendChild(div);
|
||||
const type_input = new beestat.component.input.select()
|
||||
.set_label('Type')
|
||||
.set_width('100%')
|
||||
.add_option({'label': 'Empty', 'value': 'empty'})
|
||||
.add_option({'label': 'Door', 'value': 'door'})
|
||||
.add_option({'label': 'Window', 'value': 'window'})
|
||||
.render(div);
|
||||
|
||||
type_input.set_value(['empty', 'door', 'window'].includes(opening.type) ? opening.type : 'empty');
|
||||
type_input.addEventListener('change', function() {
|
||||
opening.type = type_input.get_value();
|
||||
self.update_floor_plan_();
|
||||
self.rerender();
|
||||
});
|
||||
|
||||
// Width
|
||||
div = $.createElement('div');
|
||||
grid.appendChild(div);
|
||||
const width_input = new beestat.component.input.text()
|
||||
.set_label('Width (' + beestat.setting('units.distance') + ')')
|
||||
.set_placeholder(beestat.distance({
|
||||
'distance': opening.width || 0,
|
||||
'round': 2
|
||||
}))
|
||||
.set_value(beestat.distance({
|
||||
'distance': opening.width || 0,
|
||||
'round': 2
|
||||
}) || '')
|
||||
.set_width('100%')
|
||||
.set_maxlength(5)
|
||||
.set_requirements({
|
||||
'type': 'decimal',
|
||||
'min_value': beestat.distance(1),
|
||||
'required': true
|
||||
})
|
||||
.set_transform({
|
||||
'type': 'round',
|
||||
'decimals': 2
|
||||
})
|
||||
.render(div);
|
||||
|
||||
width_input.addEventListener('change', function() {
|
||||
if (width_input.meets_requirements() === true) {
|
||||
opening.width = beestat.distance({
|
||||
'distance': width_input.get_value(),
|
||||
'input_distance_unit': beestat.setting('units.distance'),
|
||||
'output_distance_unit': 'in',
|
||||
'round': 2
|
||||
});
|
||||
self.update_floor_plan_();
|
||||
self.rerender();
|
||||
} else {
|
||||
width_input.set_value(beestat.distance({
|
||||
'distance': opening.width || 0,
|
||||
'round': 2
|
||||
}) || '', false);
|
||||
}
|
||||
});
|
||||
|
||||
// Height
|
||||
div = $.createElement('div');
|
||||
grid.appendChild(div);
|
||||
const height_input = new beestat.component.input.text()
|
||||
.set_label('Height (' + beestat.setting('units.distance') + ')')
|
||||
.set_placeholder(beestat.distance({
|
||||
'distance': opening.height || 0,
|
||||
'round': 2
|
||||
}))
|
||||
.set_value(beestat.distance({
|
||||
'distance': opening.height || 0,
|
||||
'round': 2
|
||||
}) || '')
|
||||
.set_width('100%')
|
||||
.set_maxlength(5)
|
||||
.set_requirements({
|
||||
'type': 'decimal',
|
||||
'min_value': beestat.distance(1),
|
||||
'required': true
|
||||
})
|
||||
.set_transform({
|
||||
'type': 'round',
|
||||
'decimals': 2
|
||||
})
|
||||
.render(div);
|
||||
|
||||
height_input.addEventListener('change', function() {
|
||||
if (height_input.meets_requirements() === true) {
|
||||
opening.height = beestat.distance({
|
||||
'distance': height_input.get_value(),
|
||||
'input_distance_unit': beestat.setting('units.distance'),
|
||||
'output_distance_unit': 'in',
|
||||
'round': 2
|
||||
});
|
||||
self.update_floor_plan_();
|
||||
} else {
|
||||
height_input.set_value(beestat.distance({
|
||||
'distance': opening.height || 0,
|
||||
'round': 2
|
||||
}) || '', false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Decorate the info pane for a room.
|
||||
*
|
||||
|
||||
@ -130,7 +130,8 @@ beestat.component.floor_plan.prototype.render = function(parent) {
|
||||
if (
|
||||
self.state_.active_room_entity !== undefined ||
|
||||
self.state_.active_surface_entity !== undefined ||
|
||||
self.state_.active_tree_entity !== undefined
|
||||
self.state_.active_tree_entity !== undefined ||
|
||||
self.state_.active_opening_entity !== undefined
|
||||
) {
|
||||
self.clear_room_();
|
||||
}
|
||||
@ -152,6 +153,10 @@ beestat.component.floor_plan.prototype.render = function(parent) {
|
||||
if (e.ctrlKey === false && self.has_early_access_() === true) {
|
||||
self.add_tree_();
|
||||
}
|
||||
} else if (e.key.toLowerCase() === 'o') {
|
||||
if (e.ctrlKey === false) {
|
||||
self.add_opening_();
|
||||
}
|
||||
} else if (e.key.toLowerCase() === 's') {
|
||||
self.toggle_snapping_();
|
||||
} else if (
|
||||
@ -225,6 +230,7 @@ beestat.component.floor_plan.prototype.render = function(parent) {
|
||||
const entity =
|
||||
self.state_.active_point_entity ||
|
||||
self.state_.active_wall_entity ||
|
||||
self.state_.active_opening_entity ||
|
||||
self.state_.active_surface_entity ||
|
||||
self.state_.active_room_entity ||
|
||||
self.state_.active_tree_entity;
|
||||
@ -510,6 +516,18 @@ beestat.component.floor_plan.prototype.update_toolbar = function() {
|
||||
})
|
||||
);
|
||||
|
||||
// Add opening
|
||||
this.tile_group_.add_tile(new beestat.component.tile()
|
||||
.set_icon('window_closed_variant')
|
||||
.set_title('Add Opening [O]')
|
||||
.set_text_color(beestat.style.color.gray.light)
|
||||
.set_background_color(beestat.style.color.bluegray.base)
|
||||
.set_background_hover_color(beestat.style.color.bluegray.light)
|
||||
.addEventListener('click', function() {
|
||||
self.add_opening_();
|
||||
})
|
||||
);
|
||||
|
||||
if (this.has_early_access_() === true) {
|
||||
// Add surface
|
||||
this.tile_group_.add_tile(new beestat.component.tile()
|
||||
@ -550,7 +568,7 @@ beestat.component.floor_plan.prototype.update_toolbar = function() {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove selected room, surface, or tree
|
||||
// Remove selected room, opening, surface, or tree
|
||||
const remove_button = new beestat.component.tile()
|
||||
.set_icon('card_remove_outline')
|
||||
.set_title('Remove [Delete]')
|
||||
@ -559,6 +577,7 @@ beestat.component.floor_plan.prototype.update_toolbar = function() {
|
||||
|
||||
if (
|
||||
this.state_.active_room_entity !== undefined ||
|
||||
this.state_.active_opening_entity !== undefined ||
|
||||
this.state_.active_surface_entity !== undefined ||
|
||||
this.state_.active_tree_entity !== undefined
|
||||
) {
|
||||
@ -800,6 +819,17 @@ beestat.component.floor_plan.prototype.update_infobox = function() {
|
||||
'units': true
|
||||
})
|
||||
);
|
||||
} else if (this.state_.active_opening_entity !== undefined) {
|
||||
const opening = this.state_.active_opening_entity.get_opening();
|
||||
parts.push('Opening');
|
||||
parts.push((opening.type || 'empty').toUpperCase());
|
||||
parts.push(
|
||||
beestat.distance({
|
||||
'distance': opening.width || 0,
|
||||
'units': true,
|
||||
'round': 0
|
||||
}) + ' w'
|
||||
);
|
||||
} else {
|
||||
parts.push(this.state_.active_group.name || 'Unnamed Floor');
|
||||
parts.push(
|
||||
@ -1016,9 +1046,14 @@ beestat.component.floor_plan.prototype.remove_room_ = function() {
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the currently active selectable entity (surface, room, or tree).
|
||||
* Remove the currently active selectable entity (surface, room, opening, or tree).
|
||||
*/
|
||||
beestat.component.floor_plan.prototype.remove_active_entity_ = function() {
|
||||
if (this.state_.active_opening_entity !== undefined) {
|
||||
this.remove_opening_();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state_.active_surface_entity !== undefined) {
|
||||
this.remove_surface_();
|
||||
return;
|
||||
@ -1061,6 +1096,9 @@ beestat.component.floor_plan.prototype.set_active_group = function(group) {
|
||||
if (this.state_.active_tree_entity !== undefined) {
|
||||
this.state_.active_tree_entity.set_active(false);
|
||||
}
|
||||
if (this.state_.active_opening_entity !== undefined) {
|
||||
this.state_.active_opening_entity.set_active(false);
|
||||
}
|
||||
|
||||
this.state_.active_group = group;
|
||||
this.dispatchEvent('change_group');
|
||||
@ -1104,6 +1142,71 @@ beestat.component.floor_plan.prototype.remove_surface_ = function() {
|
||||
this.dispatchEvent('remove_surface');
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a new opening.
|
||||
*
|
||||
* @param {object} opening Optional opening to copy from.
|
||||
*/
|
||||
beestat.component.floor_plan.prototype.add_opening_ = function(opening) {
|
||||
this.save_buffer();
|
||||
|
||||
if (this.state_.active_group.openings === undefined) {
|
||||
this.state_.active_group.openings = [];
|
||||
}
|
||||
|
||||
const svg_view_box = this.view_box_;
|
||||
const width = Math.max(12, Number((opening || {}).width || 36));
|
||||
const height = Math.max(1, Number((opening || {}).height || 80));
|
||||
|
||||
const new_opening = {
|
||||
'opening_id': window.crypto.randomUUID(),
|
||||
'x': Number((opening || {}).x || (svg_view_box.x + (svg_view_box.width / 2))),
|
||||
'y': Number((opening || {}).y || (svg_view_box.y + (svg_view_box.height / 2))),
|
||||
'width': width,
|
||||
'height': height,
|
||||
'type': ['empty', 'door', 'window'].includes((opening || {}).type) ? opening.type : 'empty',
|
||||
'name': (opening || {}).name,
|
||||
'editor_hidden': false,
|
||||
'editor_locked': false
|
||||
};
|
||||
|
||||
this.state_.active_group.openings.unshift(new_opening);
|
||||
new beestat.component.floor_plan_entity.opening(this, this.state_)
|
||||
.set_opening(new_opening)
|
||||
.set_group(this.state_.active_group)
|
||||
.set_active(true);
|
||||
|
||||
this.dispatchEvent('add_opening');
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the currently active opening.
|
||||
*/
|
||||
beestat.component.floor_plan.prototype.remove_opening_ = function() {
|
||||
this.save_buffer();
|
||||
|
||||
if (
|
||||
this.state_.active_opening_entity === undefined ||
|
||||
this.state_.active_group.openings === undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
const index = this.state_.active_group.openings.findIndex(function(opening) {
|
||||
return opening === self.state_.active_opening_entity.get_opening();
|
||||
});
|
||||
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.state_.active_opening_entity.set_active(false);
|
||||
this.state_.active_group.openings.splice(index, 1);
|
||||
|
||||
this.dispatchEvent('remove_opening');
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a new tree to the first floor.
|
||||
*
|
||||
@ -1212,6 +1315,9 @@ beestat.component.floor_plan.prototype.clear_room_ = function() {
|
||||
if (this.state_.active_point_entity !== undefined) {
|
||||
this.state_.active_point_entity.set_active(false);
|
||||
}
|
||||
if (this.state_.active_opening_entity !== undefined) {
|
||||
this.state_.active_opening_entity.set_active(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -1474,6 +1580,7 @@ beestat.component.floor_plan.prototype.save_buffer = function(clear = true) {
|
||||
'active_room_entity': this.state_.active_room_entity,
|
||||
'active_surface_entity': this.state_.active_surface_entity,
|
||||
'active_tree_entity': this.state_.active_tree_entity,
|
||||
'active_opening_entity': this.state_.active_opening_entity,
|
||||
'active_group_id': this.state_.active_group.group_id
|
||||
});
|
||||
|
||||
@ -1523,6 +1630,8 @@ beestat.component.floor_plan.prototype.undo_ = function() {
|
||||
this.state_.buffer[this.state_.buffer_pointer].active_surface_entity;
|
||||
this.state_.active_tree_entity =
|
||||
this.state_.buffer[this.state_.buffer_pointer].active_tree_entity;
|
||||
this.state_.active_opening_entity =
|
||||
this.state_.buffer[this.state_.buffer_pointer].active_opening_entity;
|
||||
|
||||
// Restore any active group.
|
||||
this.state_.active_group_id =
|
||||
@ -1572,6 +1681,8 @@ beestat.component.floor_plan.prototype.redo_ = function() {
|
||||
this.state_.buffer[this.state_.buffer_pointer].active_surface_entity;
|
||||
this.state_.active_tree_entity =
|
||||
this.state_.buffer[this.state_.buffer_pointer].active_tree_entity;
|
||||
this.state_.active_opening_entity =
|
||||
this.state_.buffer[this.state_.buffer_pointer].active_opening_entity;
|
||||
|
||||
// Restore any active group.
|
||||
this.state_.active_group_id =
|
||||
@ -1600,3 +1711,5 @@ beestat.component.floor_plan.prototype.can_redo_ = function() {
|
||||
return this.state_.buffer !== undefined &&
|
||||
this.state_.buffer_pointer + 1 < this.state_.buffer.length;
|
||||
};
|
||||
|
||||
|
||||
|
||||
340
js/component/floor_plan_entity/opening.js
Normal file
340
js/component/floor_plan_entity/opening.js
Normal file
@ -0,0 +1,340 @@
|
||||
/**
|
||||
* Floor plan opening (empty, door, window).
|
||||
*/
|
||||
beestat.component.floor_plan_entity.opening = function() {
|
||||
this.enabled_ = true;
|
||||
this.resize_mode_ = null;
|
||||
|
||||
beestat.component.floor_plan_entity.apply(this, arguments);
|
||||
};
|
||||
beestat.extend(beestat.component.floor_plan_entity.opening, beestat.component.floor_plan_entity);
|
||||
|
||||
/**
|
||||
* Decorate.
|
||||
*
|
||||
* @param {SVGGElement} parent
|
||||
*/
|
||||
beestat.component.floor_plan_entity.opening.prototype.decorate_ = function(parent) {
|
||||
this.decorate_opening_(parent);
|
||||
|
||||
if (this.enabled_ === true) {
|
||||
this.set_draggable_(true);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Build opening visuals.
|
||||
*
|
||||
* @param {SVGGElement} parent
|
||||
*/
|
||||
beestat.component.floor_plan_entity.opening.prototype.decorate_opening_ = function(parent) {
|
||||
const self = this;
|
||||
|
||||
this.path_ = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
this.path_.style.fill = 'none';
|
||||
this.path_.style.strokeLinecap = 'round';
|
||||
this.path_.style.cursor = this.enabled_ === true ? 'move' : 'default';
|
||||
parent.appendChild(this.path_);
|
||||
|
||||
this.left_handle_ = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||
this.right_handle_ = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||
[this.left_handle_, this.right_handle_].forEach(function(handle, index) {
|
||||
handle.setAttribute('width', '8');
|
||||
handle.setAttribute('height', '8');
|
||||
handle.setAttribute('rx', '1');
|
||||
handle.setAttribute('ry', '1');
|
||||
handle.style.cursor = 'ew-resize';
|
||||
handle.style.strokeWidth = '1';
|
||||
parent.appendChild(handle);
|
||||
|
||||
if (self.enabled_ === true) {
|
||||
handle.addEventListener('mousedown', function(e) {
|
||||
e.stopPropagation();
|
||||
self.resize_mode_ = index === 0 ? 'left' : 'right';
|
||||
self.mousedown_handler_(e);
|
||||
});
|
||||
handle.addEventListener('touchstart', function(e) {
|
||||
e.stopPropagation();
|
||||
self.resize_mode_ = index === 0 ? 'left' : 'right';
|
||||
self.mousedown_handler_(e);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (this.enabled_ === true) {
|
||||
this.path_.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
self.set_active(true);
|
||||
});
|
||||
this.path_.addEventListener('touchstart', function(e) {
|
||||
e.stopPropagation();
|
||||
self.set_active(true);
|
||||
});
|
||||
}
|
||||
|
||||
this.update();
|
||||
this.apply_transform_();
|
||||
};
|
||||
|
||||
/**
|
||||
* Update visuals.
|
||||
*/
|
||||
beestat.component.floor_plan_entity.opening.prototype.update = function() {
|
||||
const width = Math.max(12, Number(this.opening_.width || 0));
|
||||
const half_width = width / 2;
|
||||
|
||||
this.path_.setAttribute('d', 'M' + (-half_width) + ',0 L' + half_width + ',0');
|
||||
this.path_.style.stroke = this.get_opening_color_();
|
||||
this.path_.style.strokeWidth = this.active_ === true ? '6' : '4';
|
||||
this.path_.style.opacity = this.enabled_ === true ? (this.active_ === true ? '0.95' : '0.7') : '0.3';
|
||||
|
||||
const handles_visible = this.active_ === true && this.enabled_ === true;
|
||||
const handle_fill = this.get_opening_color_();
|
||||
[this.left_handle_, this.right_handle_].forEach(function(handle) {
|
||||
handle.style.visibility = handles_visible === true ? 'visible' : 'hidden';
|
||||
handle.style.fill = handle_fill;
|
||||
handle.style.stroke = '#ffffff';
|
||||
});
|
||||
this.left_handle_.setAttribute('x', String(-half_width - 4));
|
||||
this.left_handle_.setAttribute('y', '-4');
|
||||
this.right_handle_.setAttribute('x', String(half_width - 4));
|
||||
this.right_handle_.setAttribute('y', '-4');
|
||||
|
||||
this.set_xy(this.opening_.x, this.opening_.y);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle after mousedown.
|
||||
*/
|
||||
beestat.component.floor_plan_entity.opening.prototype.after_mousedown_handler_ = function() {
|
||||
this.drag_start_entity_ = {
|
||||
'x': this.opening_.x || 0,
|
||||
'y': this.opening_.y || 0,
|
||||
'width': this.opening_.width || 0
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle dragging.
|
||||
*
|
||||
* @param {Event} e
|
||||
*/
|
||||
beestat.component.floor_plan_entity.opening.prototype.after_mousemove_handler_ = function(e) {
|
||||
const grid_half = this.floor_plan_.get_grid_pixels() / 2;
|
||||
const min_width = 12;
|
||||
|
||||
const dx = ((e.clientX || e.touches[0].clientX) - this.drag_start_mouse_.x) * this.floor_plan_.get_scale();
|
||||
const dy = ((e.clientY || e.touches[0].clientY) - this.drag_start_mouse_.y) * this.floor_plan_.get_scale();
|
||||
|
||||
if (this.resize_mode_ === 'left' || this.resize_mode_ === 'right') {
|
||||
const start_width = Math.max(min_width, Number(this.drag_start_entity_.width || 0));
|
||||
const start_left = this.drag_start_entity_.x - (start_width / 2);
|
||||
const start_right = this.drag_start_entity_.x + (start_width / 2);
|
||||
|
||||
let next_left = start_left;
|
||||
let next_right = start_right;
|
||||
|
||||
if (this.resize_mode_ === 'left') {
|
||||
next_left = Math.min(start_left + dx, start_right - min_width);
|
||||
next_left = Math.max(-grid_half, next_left);
|
||||
} else {
|
||||
next_right = Math.max(start_right + dx, start_left + min_width);
|
||||
next_right = Math.min(grid_half, next_right);
|
||||
}
|
||||
|
||||
const next_width = Math.max(min_width, next_right - next_left);
|
||||
const next_x = Math.max(-grid_half + (next_width / 2), Math.min(grid_half - (next_width / 2), (next_left + next_right) / 2));
|
||||
|
||||
this.opening_.width = Math.round(next_width);
|
||||
this.opening_.x = Math.round(next_x);
|
||||
this.set_xy(this.opening_.x, this.opening_.y);
|
||||
this.update();
|
||||
return;
|
||||
}
|
||||
|
||||
const width = Math.max(min_width, Number(this.opening_.width || 0));
|
||||
const half_width = width / 2;
|
||||
const next_x = this.drag_start_entity_.x + dx;
|
||||
const next_y = this.drag_start_entity_.y + dy;
|
||||
|
||||
this.opening_.x = Math.round(Math.max(-grid_half + half_width, Math.min(grid_half - half_width, next_x)));
|
||||
this.opening_.y = Math.round(Math.max(-grid_half, Math.min(grid_half, next_y)));
|
||||
this.set_xy(this.opening_.x, this.opening_.y);
|
||||
};
|
||||
|
||||
/**
|
||||
* Cleanup after mouseup.
|
||||
*/
|
||||
beestat.component.floor_plan_entity.opening.prototype.after_mouseup_handler_ = function() {
|
||||
this.resize_mode_ = null;
|
||||
this.update();
|
||||
};
|
||||
|
||||
/**
|
||||
* Set opening.
|
||||
*
|
||||
* @param {object} opening
|
||||
*
|
||||
* @return {beestat.component.floor_plan_entity.opening}
|
||||
*/
|
||||
beestat.component.floor_plan_entity.opening.prototype.set_opening = function(opening) {
|
||||
this.opening_ = opening;
|
||||
|
||||
if (this.opening_.opening_id === undefined) {
|
||||
this.opening_.opening_id = window.crypto.randomUUID();
|
||||
}
|
||||
if (this.opening_.type === undefined) {
|
||||
this.opening_.type = 'empty';
|
||||
}
|
||||
if (this.opening_.width === undefined) {
|
||||
this.opening_.width = 36;
|
||||
}
|
||||
if (this.opening_.height === undefined) {
|
||||
this.opening_.height = 80;
|
||||
}
|
||||
|
||||
this.set_xy(this.opening_.x || 0, this.opening_.y || 0);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set group.
|
||||
*
|
||||
* @param {object} group
|
||||
*
|
||||
* @return {beestat.component.floor_plan_entity.opening}
|
||||
*/
|
||||
beestat.component.floor_plan_entity.opening.prototype.set_group = function(group) {
|
||||
this.group_ = group;
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get opening.
|
||||
*
|
||||
* @return {object}
|
||||
*/
|
||||
beestat.component.floor_plan_entity.opening.prototype.get_opening = function() {
|
||||
return this.opening_;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set enabled.
|
||||
*
|
||||
* @param {boolean} enabled
|
||||
*
|
||||
* @return {beestat.component.floor_plan_entity.opening}
|
||||
*/
|
||||
beestat.component.floor_plan_entity.opening.prototype.set_enabled = function(enabled) {
|
||||
this.enabled_ = enabled;
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set x/y with clamping.
|
||||
*
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {string} event
|
||||
*
|
||||
* @return {beestat.component.floor_plan_entity.opening}
|
||||
*/
|
||||
beestat.component.floor_plan_entity.opening.prototype.set_xy = function(x, y, event = 'lesser_update') {
|
||||
if (event === 'update') {
|
||||
this.floor_plan_.save_buffer();
|
||||
}
|
||||
|
||||
const grid_half = this.floor_plan_.get_grid_pixels() / 2;
|
||||
const half_width = Math.max(12, Number(this.opening_.width || 0)) / 2;
|
||||
|
||||
const clamped_x = Math.max(-grid_half + half_width, Math.min(grid_half - half_width, Number(x || 0)));
|
||||
const clamped_y = Math.max(-grid_half, Math.min(grid_half, Number(y || 0)));
|
||||
|
||||
this.opening_.x = Math.round(clamped_x);
|
||||
this.opening_.y = Math.round(clamped_y);
|
||||
|
||||
beestat.component.floor_plan_entity.prototype.set_xy.apply(
|
||||
this,
|
||||
[
|
||||
this.opening_.x,
|
||||
this.opening_.y
|
||||
]
|
||||
);
|
||||
|
||||
this.dispatchEvent(event);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set active state.
|
||||
*
|
||||
* @param {boolean} active
|
||||
*
|
||||
* @return {beestat.component.floor_plan_entity.opening}
|
||||
*/
|
||||
beestat.component.floor_plan_entity.opening.prototype.set_active = function(active) {
|
||||
if (active === true && this.enabled_ !== true) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (active === true) {
|
||||
if (this.state_.active_point_entity !== undefined) {
|
||||
this.state_.active_point_entity.set_active(false);
|
||||
}
|
||||
if (this.state_.active_wall_entity !== undefined) {
|
||||
this.state_.active_wall_entity.set_active(false);
|
||||
}
|
||||
if (this.state_.active_tree_entity !== undefined) {
|
||||
this.state_.active_tree_entity.set_active(false);
|
||||
}
|
||||
if (this.state_.active_surface_entity !== undefined) {
|
||||
this.state_.active_surface_entity.set_active(false);
|
||||
}
|
||||
if (this.state_.active_room_entity !== undefined) {
|
||||
this.state_.active_room_entity.set_active(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (active !== this.active_) {
|
||||
this.active_ = active;
|
||||
|
||||
if (this.active_ === true) {
|
||||
if (
|
||||
this.state_.active_opening_entity !== undefined &&
|
||||
this.state_.active_opening_entity.get_opening() !== this.opening_
|
||||
) {
|
||||
this.state_.active_opening_entity.set_active(false);
|
||||
}
|
||||
|
||||
this.state_.active_opening_entity = this;
|
||||
this.dispatchEvent('activate');
|
||||
this.bring_to_front_();
|
||||
} else {
|
||||
delete this.state_.active_opening_entity;
|
||||
this.dispatchEvent('inactivate');
|
||||
}
|
||||
|
||||
if (this.rendered_ === true) {
|
||||
this.rerender();
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get color by opening type.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
beestat.component.floor_plan_entity.opening.prototype.get_opening_color_ = function() {
|
||||
switch (this.opening_.type) {
|
||||
case 'door':
|
||||
return beestat.style.color.green.base;
|
||||
case 'window':
|
||||
return beestat.style.color.lightblue.light;
|
||||
case 'empty':
|
||||
default:
|
||||
return beestat.style.color.gray.light;
|
||||
}
|
||||
};
|
||||
@ -256,6 +256,10 @@ beestat.component.floor_plan_entity.room.prototype.set_active = function(active)
|
||||
this.state_.active_surface_entity.set_active(false);
|
||||
this.floor_plan_.update_toolbar();
|
||||
}
|
||||
if (this.state_.active_opening_entity !== undefined) {
|
||||
this.state_.active_opening_entity.set_active(false);
|
||||
this.floor_plan_.update_toolbar();
|
||||
}
|
||||
|
||||
if (active !== this.active_) {
|
||||
this.active_ = active;
|
||||
|
||||
@ -113,6 +113,10 @@ beestat.component.floor_plan_entity.surface.prototype.set_active = function(acti
|
||||
this.state_.active_tree_entity.set_active(false);
|
||||
this.floor_plan_.update_toolbar();
|
||||
}
|
||||
if (this.state_.active_opening_entity !== undefined) {
|
||||
this.state_.active_opening_entity.set_active(false);
|
||||
this.floor_plan_.update_toolbar();
|
||||
}
|
||||
|
||||
if (active !== this.active_) {
|
||||
this.active_ = active;
|
||||
|
||||
@ -209,6 +209,9 @@ beestat.component.floor_plan_entity.tree.prototype.set_active = function(active)
|
||||
if (this.state_.active_surface_entity !== undefined) {
|
||||
this.state_.active_surface_entity.set_active(false);
|
||||
}
|
||||
if (this.state_.active_opening_entity !== undefined) {
|
||||
this.state_.active_opening_entity.set_active(false);
|
||||
}
|
||||
|
||||
this.state_.active_tree_entity = this;
|
||||
this.dispatchEvent('activate');
|
||||
|
||||
@ -95,6 +95,7 @@ beestat.component.floor_plan_layers_sidebar.prototype.decorate_ = function(paren
|
||||
sidebar_state.collapsed_groups[group.group_id] = true;
|
||||
sidebar_state.collapsed_types[group.group_id + '.trees'] = true;
|
||||
sidebar_state.collapsed_types[group.group_id + '.surfaces'] = true;
|
||||
sidebar_state.collapsed_types[group.group_id + '.openings'] = true;
|
||||
sidebar_state.collapsed_types[group.group_id + '.rooms'] = true;
|
||||
});
|
||||
sidebar_state.initialized_collapsed = true;
|
||||
@ -110,6 +111,9 @@ beestat.component.floor_plan_layers_sidebar.prototype.decorate_ = function(paren
|
||||
if (sidebar_state.collapsed_types[group.group_id + '.surfaces'] === undefined) {
|
||||
sidebar_state.collapsed_types[group.group_id + '.surfaces'] = true;
|
||||
}
|
||||
if (sidebar_state.collapsed_types[group.group_id + '.openings'] === undefined) {
|
||||
sidebar_state.collapsed_types[group.group_id + '.openings'] = true;
|
||||
}
|
||||
if (sidebar_state.collapsed_types[group.group_id + '.rooms'] === undefined) {
|
||||
sidebar_state.collapsed_types[group.group_id + '.rooms'] = true;
|
||||
}
|
||||
@ -122,6 +126,7 @@ beestat.component.floor_plan_layers_sidebar.prototype.decorate_ = function(paren
|
||||
const group_objects = []
|
||||
.concat(group.trees || [])
|
||||
.concat(group.surfaces || [])
|
||||
.concat(group.openings || [])
|
||||
.concat(group.rooms || []);
|
||||
const has_group_objects = group_objects.length > 0;
|
||||
const group_all_hidden = has_group_objects === true && group_objects.every(function(object) {
|
||||
@ -212,6 +217,9 @@ beestat.component.floor_plan_layers_sidebar.prototype.decorate_ = function(paren
|
||||
if ((group.surfaces || []).length > 0) {
|
||||
self.on_toggle_layer_visibility_(group, 'surfaces', group_all_hidden === true);
|
||||
}
|
||||
if ((group.openings || []).length > 0) {
|
||||
self.on_toggle_layer_visibility_(group, 'openings', group_all_hidden === true);
|
||||
}
|
||||
if ((group.rooms || []).length > 0) {
|
||||
self.on_toggle_layer_visibility_(group, 'rooms', group_all_hidden === true);
|
||||
}
|
||||
@ -246,6 +254,9 @@ beestat.component.floor_plan_layers_sidebar.prototype.decorate_ = function(paren
|
||||
if ((group.surfaces || []).length > 0) {
|
||||
self.on_toggle_layer_lock_(group, 'surfaces', !group_all_locked);
|
||||
}
|
||||
if ((group.openings || []).length > 0) {
|
||||
self.on_toggle_layer_lock_(group, 'openings', !group_all_locked);
|
||||
}
|
||||
if ((group.rooms || []).length > 0) {
|
||||
self.on_toggle_layer_lock_(group, 'rooms', !group_all_locked);
|
||||
}
|
||||
@ -301,6 +312,15 @@ beestat.component.floor_plan_layers_sidebar.prototype.decorate_ = function(paren
|
||||
scroll_to,
|
||||
scroll_to_row
|
||||
);
|
||||
scroll_to_row = self.decorate_group_type_(
|
||||
group_panel,
|
||||
group,
|
||||
'openings',
|
||||
'Opening',
|
||||
font_size_small,
|
||||
scroll_to,
|
||||
scroll_to_row
|
||||
);
|
||||
scroll_to_row = self.decorate_group_type_(
|
||||
group_panel,
|
||||
group,
|
||||
@ -968,6 +988,9 @@ beestat.component.floor_plan_layers_sidebar.prototype.get_type_icon_ = function(
|
||||
if (type === 'surfaces') {
|
||||
return 'texture_box';
|
||||
}
|
||||
if (type === 'openings') {
|
||||
return 'window_closed_variant';
|
||||
}
|
||||
return 'view_quilt';
|
||||
};
|
||||
|
||||
@ -1013,6 +1036,9 @@ beestat.component.floor_plan_layers_sidebar.prototype.get_object_id_ = function(
|
||||
if (type === 'surfaces') {
|
||||
return object.surface_id;
|
||||
}
|
||||
if (type === 'openings') {
|
||||
return object.opening_id;
|
||||
}
|
||||
return object.tree_id;
|
||||
};
|
||||
|
||||
@ -1080,6 +1106,13 @@ beestat.component.floor_plan_layers_sidebar.prototype.is_active_row_ = function(
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
type === 'openings' &&
|
||||
this.state_.active_opening_entity !== undefined &&
|
||||
this.state_.active_opening_entity.get_opening().opening_id === object_id
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
@ -508,7 +508,9 @@ beestat.component.scene.prototype.decorate_ = function(parent) {
|
||||
'moon_light_helper': false,
|
||||
'watcher': false,
|
||||
'roof_edges': false,
|
||||
'straight_skeleton': false
|
||||
'straight_skeleton': false,
|
||||
'openings': true,
|
||||
'opening_cutters': false
|
||||
};
|
||||
|
||||
this.width_ = this.state_.scene_width || 800;
|
||||
@ -2138,6 +2140,8 @@ beestat.component.scene.prototype.add_walls_ = function(layer, group) {
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
mesh.position.z = -wall_height - elevation;
|
||||
mesh.userData.is_wall = true;
|
||||
mesh.userData.group_id = group.group_id;
|
||||
mesh.userData.wall_cuttable = true;
|
||||
mesh.layers.set(beestat.component.scene.layer_visible);
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
@ -2147,6 +2151,206 @@ beestat.component.scene.prototype.add_walls_ = function(layer, group) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Build an opening cutter mesh for CSG subtraction.
|
||||
*
|
||||
* @param {object} group The floor plan group.
|
||||
* @param {object} opening The opening.
|
||||
* @return {?THREE.Mesh} Opening cutter mesh or null if opening is not cuttable.
|
||||
*/
|
||||
beestat.component.scene.prototype.build_opening_cutter_mesh_ = function(group, opening) {
|
||||
if (opening.editor_hidden === true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const width = Math.max(12, Number(opening.width || 0));
|
||||
const height = Math.max(1, Number(opening.height || 0));
|
||||
const wall_thickness = Number(beestat.component.scene.wall_thickness || 4);
|
||||
const depth = Math.max(0.5, wall_thickness);
|
||||
const elevation = Number(group.elevation || 0);
|
||||
|
||||
if (this.csg_cutter_material_ === undefined) {
|
||||
this.csg_cutter_material_ = new THREE.MeshBasicMaterial({
|
||||
'visible': false
|
||||
});
|
||||
}
|
||||
|
||||
const geometry = new THREE.BoxGeometry(width, depth, height);
|
||||
const cutter = new THREE.Mesh(geometry, this.csg_cutter_material_);
|
||||
cutter.position.set(
|
||||
Number(opening.x || 0),
|
||||
Number(opening.y || 0),
|
||||
-elevation - (height / 2)
|
||||
);
|
||||
cutter.updateMatrix();
|
||||
cutter.updateMatrixWorld(true);
|
||||
|
||||
return cutter;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a debug wireframe for an opening cutter.
|
||||
*
|
||||
* @param {THREE.Group} layer The debug layer.
|
||||
* @param {THREE.Mesh} cutter The cutter mesh.
|
||||
*/
|
||||
beestat.component.scene.prototype.add_opening_cutter_debug_ = function(layer, cutter) {
|
||||
const edges_geometry = new THREE.EdgesGeometry(cutter.geometry);
|
||||
const wireframe = new THREE.LineSegments(
|
||||
edges_geometry,
|
||||
new THREE.LineBasicMaterial({
|
||||
'color': 0xff7700
|
||||
})
|
||||
);
|
||||
wireframe.position.copy(cutter.position);
|
||||
wireframe.rotation.copy(cutter.rotation);
|
||||
wireframe.scale.copy(cutter.scale);
|
||||
wireframe.layers.set(beestat.component.scene.layer_visible);
|
||||
|
||||
layer.add(wireframe);
|
||||
};
|
||||
|
||||
/**
|
||||
* Subtract opening cutters from wall meshes.
|
||||
*
|
||||
* @param {THREE.Group} walls_layer The wall mesh layer.
|
||||
* @param {object} floor_plan The floor plan data.
|
||||
* @param {THREE.Group=} opening_cutter_debug_layer Optional debug cutter layer.
|
||||
*/
|
||||
beestat.component.scene.prototype.apply_opening_cuts_ = function(
|
||||
walls_layer,
|
||||
floor_plan,
|
||||
opening_cutter_debug_layer
|
||||
) {
|
||||
if (window.CSG === undefined || typeof window.CSG.subtract !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
const wall_meshes = walls_layer.children.filter(function(child) {
|
||||
return (
|
||||
child !== undefined &&
|
||||
child.type === 'Mesh' &&
|
||||
child.userData !== undefined &&
|
||||
child.userData.wall_cuttable === true
|
||||
);
|
||||
});
|
||||
|
||||
floor_plan.data.groups.forEach(function(group) {
|
||||
const openings = group.openings || [];
|
||||
if (openings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const group_wall_meshes = wall_meshes.filter(function(mesh) {
|
||||
return mesh.userData.group_id === group.group_id;
|
||||
});
|
||||
if (group_wall_meshes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
openings.forEach((opening) => {
|
||||
const cutter = this.build_opening_cutter_mesh_(group, opening);
|
||||
if (cutter === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (opening_cutter_debug_layer !== undefined) {
|
||||
this.add_opening_cutter_debug_(opening_cutter_debug_layer, cutter);
|
||||
}
|
||||
|
||||
const cutter_box = new THREE.Box3().setFromObject(cutter);
|
||||
|
||||
group_wall_meshes.forEach(function(wall_mesh) {
|
||||
const wall_box = new THREE.Box3().setFromObject(wall_mesh);
|
||||
if (wall_box.intersectsBox(cutter_box) !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
wall_mesh.updateMatrix();
|
||||
wall_mesh.updateMatrixWorld(true);
|
||||
|
||||
const result_mesh = window.CSG.subtract(wall_mesh, cutter);
|
||||
if (
|
||||
result_mesh === undefined ||
|
||||
result_mesh.geometry === undefined ||
|
||||
result_mesh.geometry.attributes === undefined ||
|
||||
result_mesh.geometry.attributes.position === undefined ||
|
||||
result_mesh.geometry.attributes.position.count === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
result_mesh.geometry.computeBoundingBox();
|
||||
result_mesh.geometry.computeBoundingSphere();
|
||||
result_mesh.geometry.computeVertexNormals();
|
||||
|
||||
const old_geometry = wall_mesh.geometry;
|
||||
wall_mesh.geometry = result_mesh.geometry;
|
||||
wall_mesh.castShadow = true;
|
||||
wall_mesh.receiveShadow = true;
|
||||
wall_mesh.layers.set(beestat.component.scene.layer_visible);
|
||||
wall_mesh.updateMatrix();
|
||||
wall_mesh.updateMatrixWorld(true);
|
||||
|
||||
if (old_geometry !== undefined) {
|
||||
old_geometry.dispose();
|
||||
}
|
||||
} catch (error) {
|
||||
// Keep original wall mesh if CSG subtraction fails.
|
||||
}
|
||||
});
|
||||
|
||||
cutter.geometry.dispose();
|
||||
});
|
||||
}, this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add red wireframe boxes to visualize opening placement in 3D.
|
||||
*
|
||||
* @param {THREE.Group} layer The layer to add opening debug to.
|
||||
* @param {object} group The floor plan group.
|
||||
*/
|
||||
beestat.component.scene.prototype.add_openings_debug_ = function(layer, group) {
|
||||
if (group.openings === undefined || group.openings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wall_thickness = beestat.component.scene.wall_thickness;
|
||||
|
||||
group.openings.forEach(function(opening) {
|
||||
if (opening.editor_hidden === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const width = Math.max(12, Number(opening.width || 0));
|
||||
const height = Math.max(1, Number(opening.height || 0));
|
||||
const elevation = group.elevation || 0;
|
||||
|
||||
const geometry = new THREE.BoxGeometry(
|
||||
width,
|
||||
wall_thickness,
|
||||
height
|
||||
);
|
||||
|
||||
const edges_geometry = new THREE.EdgesGeometry(geometry);
|
||||
const wireframe = new THREE.LineSegments(
|
||||
edges_geometry,
|
||||
new THREE.LineBasicMaterial({
|
||||
'color': 0xff0000
|
||||
})
|
||||
);
|
||||
|
||||
wireframe.position.x = Number(opening.x || 0);
|
||||
wireframe.position.y = Number(opening.y || 0);
|
||||
wireframe.position.z = -elevation - (height / 2);
|
||||
wireframe.layers.set(beestat.component.scene.layer_visible);
|
||||
|
||||
layer.add(wireframe);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a helpful debug window that can be refreshed with the contents of
|
||||
* this.debug_info_.
|
||||
@ -2267,6 +2471,29 @@ beestat.component.scene.prototype.add_floor_plan_ = function() {
|
||||
self.add_walls_(walls_layer, group);
|
||||
});
|
||||
|
||||
let opening_cutter_debug_layer;
|
||||
if (this.debug_.opening_cutters === true) {
|
||||
opening_cutter_debug_layer = new THREE.Group();
|
||||
this.floor_plan_group_.add(opening_cutter_debug_layer);
|
||||
this.layers_['opening_cutters_debug'] = opening_cutter_debug_layer;
|
||||
}
|
||||
|
||||
this.apply_opening_cuts_(
|
||||
walls_layer,
|
||||
floor_plan,
|
||||
opening_cutter_debug_layer
|
||||
);
|
||||
|
||||
if (this.debug_.openings === true) {
|
||||
const openings_debug_layer = new THREE.Group();
|
||||
this.floor_plan_group_.add(openings_debug_layer);
|
||||
this.layers_['openings_debug'] = openings_debug_layer;
|
||||
|
||||
floor_plan.data.groups.forEach(function(group) {
|
||||
self.add_openings_debug_(openings_debug_layer, group);
|
||||
});
|
||||
}
|
||||
|
||||
// Add roofs using straight skeleton
|
||||
this.add_roofs_();
|
||||
|
||||
@ -4645,6 +4872,9 @@ beestat.component.scene.prototype.dispose = function() {
|
||||
if (this.star_texture_ !== undefined) {
|
||||
this.star_texture_.dispose();
|
||||
}
|
||||
if (this.csg_cutter_material_ !== undefined) {
|
||||
this.csg_cutter_material_.dispose();
|
||||
}
|
||||
|
||||
// Clean up THREE.js scene resources
|
||||
if (this.scene_ !== undefined) {
|
||||
|
||||
10
js/js.php
10
js/js.php
@ -17,10 +17,11 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
|
||||
echo '<script src="/js/lib/highcharts/highcharts.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/lib/highcharts/highcharts-more.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/lib/highcharts/exporting.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/lib/highcharts/offline-exporting.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/lib/highcharts/boost.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/lib/threejs/threejs.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/lib/suncalc/suncalc.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/lib/highcharts/offline-exporting.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/lib/highcharts/boost.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/lib/threejs/threejs.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/lib/three-csg-ts/three-csg-ts.global.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/lib/suncalc/suncalc.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/lib/clipper/clipper.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/lib/polylabel/polylabel.js"></script>' . PHP_EOL;
|
||||
echo '<script type="module" src="/js/lib/straight-skeleton/wrapper.js?' . $setting->get('commit') . '"></script>' . PHP_EOL;
|
||||
@ -172,6 +173,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
|
||||
echo '<script src="/js/component/floor_plan_entity.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/floor_plan_entity/room.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/floor_plan_entity/surface.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/floor_plan_entity/opening.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/floor_plan_entity/tree.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/floor_plan_entity/point.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/floor_plan_entity/wall.js"></script>' . PHP_EOL;
|
||||
|
||||
541
js/lib/three-csg-ts/three-csg-ts.global.js
Normal file
541
js/lib/three-csg-ts/three-csg-ts.global.js
Normal file
@ -0,0 +1,541 @@
|
||||
var BeestatCSG = (() => {
|
||||
// three-shim.js
|
||||
var BufferAttribute = window.THREE.BufferAttribute;
|
||||
var BufferGeometry = window.THREE.BufferGeometry;
|
||||
var Matrix3 = window.THREE.Matrix3;
|
||||
var Matrix4 = window.THREE.Matrix4;
|
||||
var Mesh = window.THREE.Mesh;
|
||||
var Vector3 = window.THREE.Vector3;
|
||||
|
||||
// node_modules/three-csg-ts/lib/esm/NBuf.js
|
||||
var NBuf3 = class {
|
||||
constructor(ct) {
|
||||
this.top = 0;
|
||||
this.array = new Float32Array(ct);
|
||||
}
|
||||
write(v) {
|
||||
this.array[this.top++] = v.x;
|
||||
this.array[this.top++] = v.y;
|
||||
this.array[this.top++] = v.z;
|
||||
}
|
||||
};
|
||||
var NBuf2 = class {
|
||||
constructor(ct) {
|
||||
this.top = 0;
|
||||
this.array = new Float32Array(ct);
|
||||
}
|
||||
write(v) {
|
||||
this.array[this.top++] = v.x;
|
||||
this.array[this.top++] = v.y;
|
||||
}
|
||||
};
|
||||
|
||||
// node_modules/three-csg-ts/lib/esm/Node.js
|
||||
var Node = class _Node {
|
||||
constructor(polygons) {
|
||||
this.plane = null;
|
||||
this.front = null;
|
||||
this.back = null;
|
||||
this.polygons = [];
|
||||
if (polygons)
|
||||
this.build(polygons);
|
||||
}
|
||||
clone() {
|
||||
const node = new _Node();
|
||||
node.plane = this.plane && this.plane.clone();
|
||||
node.front = this.front && this.front.clone();
|
||||
node.back = this.back && this.back.clone();
|
||||
node.polygons = this.polygons.map((p) => p.clone());
|
||||
return node;
|
||||
}
|
||||
// Convert solid space to empty space and empty space to solid space.
|
||||
invert() {
|
||||
for (let i = 0; i < this.polygons.length; i++)
|
||||
this.polygons[i].flip();
|
||||
this.plane && this.plane.flip();
|
||||
this.front && this.front.invert();
|
||||
this.back && this.back.invert();
|
||||
const temp = this.front;
|
||||
this.front = this.back;
|
||||
this.back = temp;
|
||||
}
|
||||
// Recursively remove all polygons in `polygons` that are inside this BSP
|
||||
// tree.
|
||||
clipPolygons(polygons) {
|
||||
if (!this.plane)
|
||||
return polygons.slice();
|
||||
let front = new Array(), back = new Array();
|
||||
for (let i = 0; i < polygons.length; i++) {
|
||||
this.plane.splitPolygon(polygons[i], front, back, front, back);
|
||||
}
|
||||
if (this.front)
|
||||
front = this.front.clipPolygons(front);
|
||||
this.back ? back = this.back.clipPolygons(back) : back = [];
|
||||
return front.concat(back);
|
||||
}
|
||||
// Remove all polygons in this BSP tree that are inside the other BSP tree
|
||||
// `bsp`.
|
||||
clipTo(bsp) {
|
||||
this.polygons = bsp.clipPolygons(this.polygons);
|
||||
if (this.front)
|
||||
this.front.clipTo(bsp);
|
||||
if (this.back)
|
||||
this.back.clipTo(bsp);
|
||||
}
|
||||
// Return a list of all polygons in this BSP tree.
|
||||
allPolygons() {
|
||||
let polygons = this.polygons.slice();
|
||||
if (this.front)
|
||||
polygons = polygons.concat(this.front.allPolygons());
|
||||
if (this.back)
|
||||
polygons = polygons.concat(this.back.allPolygons());
|
||||
return polygons;
|
||||
}
|
||||
// Build a BSP tree out of `polygons`. When called on an existing tree, the
|
||||
// new polygons are filtered down to the bottom of the tree and become new
|
||||
// nodes there. Each set of polygons is partitioned using the first polygon
|
||||
// (no heuristic is used to pick a good split).
|
||||
build(polygons) {
|
||||
if (!polygons.length)
|
||||
return;
|
||||
if (!this.plane)
|
||||
this.plane = polygons[0].plane.clone();
|
||||
const front = [], back = [];
|
||||
for (let i = 0; i < polygons.length; i++) {
|
||||
this.plane.splitPolygon(polygons[i], this.polygons, this.polygons, front, back);
|
||||
}
|
||||
if (front.length) {
|
||||
if (!this.front)
|
||||
this.front = new _Node();
|
||||
this.front.build(front);
|
||||
}
|
||||
if (back.length) {
|
||||
if (!this.back)
|
||||
this.back = new _Node();
|
||||
this.back.build(back);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// node_modules/three-csg-ts/lib/esm/Vector.js
|
||||
var Vector = class _Vector {
|
||||
constructor(x = 0, y = 0, z = 0) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
copy(v) {
|
||||
this.x = v.x;
|
||||
this.y = v.y;
|
||||
this.z = v.z;
|
||||
return this;
|
||||
}
|
||||
clone() {
|
||||
return new _Vector(this.x, this.y, this.z);
|
||||
}
|
||||
negate() {
|
||||
this.x *= -1;
|
||||
this.y *= -1;
|
||||
this.z *= -1;
|
||||
return this;
|
||||
}
|
||||
add(a) {
|
||||
this.x += a.x;
|
||||
this.y += a.y;
|
||||
this.z += a.z;
|
||||
return this;
|
||||
}
|
||||
sub(a) {
|
||||
this.x -= a.x;
|
||||
this.y -= a.y;
|
||||
this.z -= a.z;
|
||||
return this;
|
||||
}
|
||||
times(a) {
|
||||
this.x *= a;
|
||||
this.y *= a;
|
||||
this.z *= a;
|
||||
return this;
|
||||
}
|
||||
dividedBy(a) {
|
||||
this.x /= a;
|
||||
this.y /= a;
|
||||
this.z /= a;
|
||||
return this;
|
||||
}
|
||||
lerp(a, t) {
|
||||
return this.add(new _Vector().copy(a).sub(this).times(t));
|
||||
}
|
||||
unit() {
|
||||
return this.dividedBy(this.length());
|
||||
}
|
||||
length() {
|
||||
return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2) + Math.pow(this.z, 2));
|
||||
}
|
||||
normalize() {
|
||||
return this.unit();
|
||||
}
|
||||
cross(b) {
|
||||
const a = this.clone();
|
||||
const ax = a.x, ay = a.y, az = a.z;
|
||||
const bx = b.x, by = b.y, bz = b.z;
|
||||
this.x = ay * bz - az * by;
|
||||
this.y = az * bx - ax * bz;
|
||||
this.z = ax * by - ay * bx;
|
||||
return this;
|
||||
}
|
||||
dot(b) {
|
||||
return this.x * b.x + this.y * b.y + this.z * b.z;
|
||||
}
|
||||
toVector3() {
|
||||
return new Vector3(this.x, this.y, this.z);
|
||||
}
|
||||
};
|
||||
|
||||
// node_modules/three-csg-ts/lib/esm/Plane.js
|
||||
var Plane = class _Plane {
|
||||
constructor(normal, w) {
|
||||
this.normal = normal;
|
||||
this.w = w;
|
||||
this.normal = normal;
|
||||
this.w = w;
|
||||
}
|
||||
clone() {
|
||||
return new _Plane(this.normal.clone(), this.w);
|
||||
}
|
||||
flip() {
|
||||
this.normal.negate();
|
||||
this.w = -this.w;
|
||||
}
|
||||
// Split `polygon` by this plane if needed, then put the polygon or polygon
|
||||
// fragments in the appropriate lists. Coplanar polygons go into either
|
||||
// `coplanarFront` or `coplanarBack` depending on their orientation with
|
||||
// respect to this plane. Polygons in front or in back of this plane go into
|
||||
// either `front` or `back`.
|
||||
splitPolygon(polygon, coplanarFront, coplanarBack, front, back) {
|
||||
const COPLANAR = 0;
|
||||
const FRONT = 1;
|
||||
const BACK = 2;
|
||||
const SPANNING = 3;
|
||||
let polygonType = 0;
|
||||
const types = [];
|
||||
for (let i = 0; i < polygon.vertices.length; i++) {
|
||||
const t = this.normal.dot(polygon.vertices[i].pos) - this.w;
|
||||
const type = t < -_Plane.EPSILON ? BACK : t > _Plane.EPSILON ? FRONT : COPLANAR;
|
||||
polygonType |= type;
|
||||
types.push(type);
|
||||
}
|
||||
switch (polygonType) {
|
||||
case COPLANAR:
|
||||
(this.normal.dot(polygon.plane.normal) > 0 ? coplanarFront : coplanarBack).push(polygon);
|
||||
break;
|
||||
case FRONT:
|
||||
front.push(polygon);
|
||||
break;
|
||||
case BACK:
|
||||
back.push(polygon);
|
||||
break;
|
||||
case SPANNING: {
|
||||
const f = [], b = [];
|
||||
for (let i = 0; i < polygon.vertices.length; i++) {
|
||||
const j = (i + 1) % polygon.vertices.length;
|
||||
const ti = types[i], tj = types[j];
|
||||
const vi = polygon.vertices[i], vj = polygon.vertices[j];
|
||||
if (ti != BACK)
|
||||
f.push(vi);
|
||||
if (ti != FRONT)
|
||||
b.push(ti != BACK ? vi.clone() : vi);
|
||||
if ((ti | tj) == SPANNING) {
|
||||
const t = (this.w - this.normal.dot(vi.pos)) / this.normal.dot(new Vector().copy(vj.pos).sub(vi.pos));
|
||||
const v = vi.interpolate(vj, t);
|
||||
f.push(v);
|
||||
b.push(v.clone());
|
||||
}
|
||||
}
|
||||
if (f.length >= 3)
|
||||
front.push(new Polygon(f, polygon.shared));
|
||||
if (b.length >= 3)
|
||||
back.push(new Polygon(b, polygon.shared));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
static fromPoints(a, b, c) {
|
||||
const n = new Vector().copy(b).sub(a).cross(new Vector().copy(c).sub(a)).normalize();
|
||||
return new _Plane(n.clone(), n.dot(a));
|
||||
}
|
||||
};
|
||||
Plane.EPSILON = 1e-5;
|
||||
|
||||
// node_modules/three-csg-ts/lib/esm/Polygon.js
|
||||
var Polygon = class _Polygon {
|
||||
constructor(vertices, shared) {
|
||||
this.vertices = vertices;
|
||||
this.shared = shared;
|
||||
this.plane = Plane.fromPoints(vertices[0].pos, vertices[1].pos, vertices[2].pos);
|
||||
}
|
||||
clone() {
|
||||
return new _Polygon(this.vertices.map((v) => v.clone()), this.shared);
|
||||
}
|
||||
flip() {
|
||||
this.vertices.reverse().map((v) => v.flip());
|
||||
this.plane.flip();
|
||||
}
|
||||
};
|
||||
|
||||
// node_modules/three-csg-ts/lib/esm/Vertex.js
|
||||
var Vertex = class _Vertex {
|
||||
constructor(pos, normal, uv, color) {
|
||||
this.pos = new Vector().copy(pos);
|
||||
this.normal = new Vector().copy(normal);
|
||||
this.uv = new Vector().copy(uv);
|
||||
this.uv.z = 0;
|
||||
color && (this.color = new Vector().copy(color));
|
||||
}
|
||||
clone() {
|
||||
return new _Vertex(this.pos, this.normal, this.uv, this.color);
|
||||
}
|
||||
// Invert all orientation-specific data (e.g. vertex normal). Called when the
|
||||
// orientation of a polygon is flipped.
|
||||
flip() {
|
||||
this.normal.negate();
|
||||
}
|
||||
// Create a new vertex between this vertex and `other` by linearly
|
||||
// interpolating all properties using a parameter of `t`. Subclasses should
|
||||
// override this to interpolate additional properties.
|
||||
interpolate(other, t) {
|
||||
return new _Vertex(this.pos.clone().lerp(other.pos, t), this.normal.clone().lerp(other.normal, t), this.uv.clone().lerp(other.uv, t), this.color && other.color && this.color.clone().lerp(other.color, t));
|
||||
}
|
||||
};
|
||||
|
||||
// node_modules/three-csg-ts/lib/esm/CSG.js
|
||||
var CSG = class _CSG {
|
||||
constructor() {
|
||||
this.polygons = [];
|
||||
}
|
||||
static fromPolygons(polygons) {
|
||||
const csg = new _CSG();
|
||||
csg.polygons = polygons;
|
||||
return csg;
|
||||
}
|
||||
static fromGeometry(geom, objectIndex) {
|
||||
let polys = [];
|
||||
const posattr = geom.attributes.position;
|
||||
const normalattr = geom.attributes.normal;
|
||||
const uvattr = geom.attributes.uv;
|
||||
const colorattr = geom.attributes.color;
|
||||
const grps = geom.groups;
|
||||
let index;
|
||||
if (geom.index) {
|
||||
index = geom.index.array;
|
||||
} else {
|
||||
index = new Uint16Array(posattr.array.length / posattr.itemSize | 0);
|
||||
for (let i = 0; i < index.length; i++)
|
||||
index[i] = i;
|
||||
}
|
||||
const triCount = index.length / 3 | 0;
|
||||
polys = new Array(triCount);
|
||||
for (let i = 0, pli = 0, l = index.length; i < l; i += 3, pli++) {
|
||||
const vertices = new Array(3);
|
||||
for (let j = 0; j < 3; j++) {
|
||||
const vi = index[i + j];
|
||||
const vp = vi * 3;
|
||||
const vt = vi * 2;
|
||||
const x = posattr.array[vp];
|
||||
const y = posattr.array[vp + 1];
|
||||
const z = posattr.array[vp + 2];
|
||||
const nx = normalattr.array[vp];
|
||||
const ny = normalattr.array[vp + 1];
|
||||
const nz = normalattr.array[vp + 2];
|
||||
const u = uvattr === null || uvattr === void 0 ? void 0 : uvattr.array[vt];
|
||||
const v = uvattr === null || uvattr === void 0 ? void 0 : uvattr.array[vt + 1];
|
||||
vertices[j] = new Vertex(new Vector(x, y, z), new Vector(nx, ny, nz), new Vector(u, v, 0), colorattr && new Vector(colorattr.array[vp], colorattr.array[vp + 1], colorattr.array[vp + 2]));
|
||||
}
|
||||
if (objectIndex === void 0 && grps && grps.length > 0) {
|
||||
for (const grp of grps) {
|
||||
if (i >= grp.start && i < grp.start + grp.count) {
|
||||
polys[pli] = new Polygon(vertices, grp.materialIndex);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
polys[pli] = new Polygon(vertices, objectIndex);
|
||||
}
|
||||
}
|
||||
return _CSG.fromPolygons(polys.filter((p) => !Number.isNaN(p.plane.normal.x)));
|
||||
}
|
||||
static toGeometry(csg, toMatrix) {
|
||||
let triCount = 0;
|
||||
const ps = csg.polygons;
|
||||
for (const p of ps) {
|
||||
triCount += p.vertices.length - 2;
|
||||
}
|
||||
const geom = new BufferGeometry();
|
||||
const vertices = new NBuf3(triCount * 3 * 3);
|
||||
const normals = new NBuf3(triCount * 3 * 3);
|
||||
const uvs = new NBuf2(triCount * 2 * 3);
|
||||
let colors;
|
||||
const grps = [];
|
||||
const dgrp = [];
|
||||
for (const p of ps) {
|
||||
const pvs = p.vertices;
|
||||
const pvlen = pvs.length;
|
||||
if (p.shared !== void 0) {
|
||||
if (!grps[p.shared])
|
||||
grps[p.shared] = [];
|
||||
}
|
||||
if (pvlen && pvs[0].color !== void 0) {
|
||||
if (!colors)
|
||||
colors = new NBuf3(triCount * 3 * 3);
|
||||
}
|
||||
for (let j = 3; j <= pvlen; j++) {
|
||||
const grp = p.shared === void 0 ? dgrp : grps[p.shared];
|
||||
grp.push(vertices.top / 3, vertices.top / 3 + 1, vertices.top / 3 + 2);
|
||||
vertices.write(pvs[0].pos);
|
||||
vertices.write(pvs[j - 2].pos);
|
||||
vertices.write(pvs[j - 1].pos);
|
||||
normals.write(pvs[0].normal);
|
||||
normals.write(pvs[j - 2].normal);
|
||||
normals.write(pvs[j - 1].normal);
|
||||
if (uvs) {
|
||||
uvs.write(pvs[0].uv);
|
||||
uvs.write(pvs[j - 2].uv);
|
||||
uvs.write(pvs[j - 1].uv);
|
||||
}
|
||||
if (colors) {
|
||||
colors.write(pvs[0].color);
|
||||
colors.write(pvs[j - 2].color);
|
||||
colors.write(pvs[j - 1].color);
|
||||
}
|
||||
}
|
||||
}
|
||||
geom.setAttribute("position", new BufferAttribute(vertices.array, 3));
|
||||
geom.setAttribute("normal", new BufferAttribute(normals.array, 3));
|
||||
uvs && geom.setAttribute("uv", new BufferAttribute(uvs.array, 2));
|
||||
colors && geom.setAttribute("color", new BufferAttribute(colors.array, 3));
|
||||
for (let gi = 0; gi < grps.length; gi++) {
|
||||
if (grps[gi] === void 0) {
|
||||
grps[gi] = [];
|
||||
}
|
||||
}
|
||||
if (grps.length) {
|
||||
let index = [];
|
||||
let gbase = 0;
|
||||
for (let gi = 0; gi < grps.length; gi++) {
|
||||
geom.addGroup(gbase, grps[gi].length, gi);
|
||||
gbase += grps[gi].length;
|
||||
index = index.concat(grps[gi]);
|
||||
}
|
||||
geom.addGroup(gbase, dgrp.length, grps.length);
|
||||
index = index.concat(dgrp);
|
||||
geom.setIndex(index);
|
||||
}
|
||||
const inv = new Matrix4().copy(toMatrix).invert();
|
||||
geom.applyMatrix4(inv);
|
||||
geom.computeBoundingSphere();
|
||||
geom.computeBoundingBox();
|
||||
return geom;
|
||||
}
|
||||
static fromMesh(mesh, objectIndex) {
|
||||
const csg = _CSG.fromGeometry(mesh.geometry, objectIndex);
|
||||
const ttvv0 = new Vector3();
|
||||
const tmpm3 = new Matrix3();
|
||||
tmpm3.getNormalMatrix(mesh.matrix);
|
||||
for (let i = 0; i < csg.polygons.length; i++) {
|
||||
const p = csg.polygons[i];
|
||||
for (let j = 0; j < p.vertices.length; j++) {
|
||||
const v = p.vertices[j];
|
||||
v.pos.copy(ttvv0.copy(v.pos.toVector3()).applyMatrix4(mesh.matrix));
|
||||
v.normal.copy(ttvv0.copy(v.normal.toVector3()).applyMatrix3(tmpm3));
|
||||
}
|
||||
}
|
||||
return csg;
|
||||
}
|
||||
static toMesh(csg, toMatrix, toMaterial) {
|
||||
const geom = _CSG.toGeometry(csg, toMatrix);
|
||||
const m = new Mesh(geom, toMaterial);
|
||||
m.matrix.copy(toMatrix);
|
||||
m.matrix.decompose(m.position, m.quaternion, m.scale);
|
||||
m.rotation.setFromQuaternion(m.quaternion);
|
||||
m.updateMatrixWorld();
|
||||
m.castShadow = m.receiveShadow = true;
|
||||
return m;
|
||||
}
|
||||
static union(meshA, meshB) {
|
||||
const csgA = _CSG.fromMesh(meshA);
|
||||
const csgB = _CSG.fromMesh(meshB);
|
||||
return _CSG.toMesh(csgA.union(csgB), meshA.matrix, meshA.material);
|
||||
}
|
||||
static subtract(meshA, meshB) {
|
||||
const csgA = _CSG.fromMesh(meshA);
|
||||
const csgB = _CSG.fromMesh(meshB);
|
||||
return _CSG.toMesh(csgA.subtract(csgB), meshA.matrix, meshA.material);
|
||||
}
|
||||
static intersect(meshA, meshB) {
|
||||
const csgA = _CSG.fromMesh(meshA);
|
||||
const csgB = _CSG.fromMesh(meshB);
|
||||
return _CSG.toMesh(csgA.intersect(csgB), meshA.matrix, meshA.material);
|
||||
}
|
||||
clone() {
|
||||
const csg = new _CSG();
|
||||
csg.polygons = this.polygons.map((p) => p.clone()).filter((p) => Number.isFinite(p.plane.w));
|
||||
return csg;
|
||||
}
|
||||
toPolygons() {
|
||||
return this.polygons;
|
||||
}
|
||||
union(csg) {
|
||||
const a = new Node(this.clone().polygons);
|
||||
const b = new Node(csg.clone().polygons);
|
||||
a.clipTo(b);
|
||||
b.clipTo(a);
|
||||
b.invert();
|
||||
b.clipTo(a);
|
||||
b.invert();
|
||||
a.build(b.allPolygons());
|
||||
return _CSG.fromPolygons(a.allPolygons());
|
||||
}
|
||||
subtract(csg) {
|
||||
const a = new Node(this.clone().polygons);
|
||||
const b = new Node(csg.clone().polygons);
|
||||
a.invert();
|
||||
a.clipTo(b);
|
||||
b.clipTo(a);
|
||||
b.invert();
|
||||
b.clipTo(a);
|
||||
b.invert();
|
||||
a.build(b.allPolygons());
|
||||
a.invert();
|
||||
return _CSG.fromPolygons(a.allPolygons());
|
||||
}
|
||||
intersect(csg) {
|
||||
const a = new Node(this.clone().polygons);
|
||||
const b = new Node(csg.clone().polygons);
|
||||
a.invert();
|
||||
b.clipTo(a);
|
||||
b.invert();
|
||||
a.clipTo(b);
|
||||
b.clipTo(a);
|
||||
a.build(b.allPolygons());
|
||||
a.invert();
|
||||
return _CSG.fromPolygons(a.allPolygons());
|
||||
}
|
||||
// Return a new CSG solid with solid and empty space switched. This solid is
|
||||
// not modified.
|
||||
inverse() {
|
||||
const csg = this.clone();
|
||||
for (const p of csg.polygons) {
|
||||
p.flip();
|
||||
}
|
||||
return csg;
|
||||
}
|
||||
toMesh(toMatrix, toMaterial) {
|
||||
return _CSG.toMesh(this, toMatrix, toMaterial);
|
||||
}
|
||||
toGeometry(toMatrix) {
|
||||
return _CSG.toGeometry(this, toMatrix);
|
||||
}
|
||||
};
|
||||
|
||||
// entry.js
|
||||
window.CSG = CSG;
|
||||
})();
|
||||
Loading…
x
Reference in New Issue
Block a user