1
0
mirror of https://github.com/beestat/app.git synced 2026-02-26 05:00:21 -05:00
This commit is contained in:
Jon Ziebell 2026-02-19 08:59:36 -05:00
parent 1c714aa05b
commit 42be5e4d7d
10 changed files with 1573 additions and 19 deletions

View File

@ -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.
*

View File

@ -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;
};

View 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;
}
};

View File

@ -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;

View File

@ -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;

View File

@ -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');

View File

@ -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;
};

View File

@ -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) {

View File

@ -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;

View 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;
})();