mirror of
https://github.com/beestat/app.git
synced 2026-02-26 13:10:23 -05:00
656 lines
21 KiB
JavaScript
656 lines
21 KiB
JavaScript
/**
|
|
* Scene methods split from scene.js.
|
|
*/
|
|
|
|
|
|
/**
|
|
* Get the ceiling Z-position for a room.
|
|
*
|
|
* @param {object} group The floor plan group
|
|
* @param {object} room The room
|
|
*
|
|
* @return {number} The ceiling Z position
|
|
*/
|
|
beestat.component.scene.prototype.get_ceiling_z_ = function(group, room) {
|
|
const elevation = room.elevation || group.elevation || 0;
|
|
const height = room.height || group.height || 96;
|
|
return -(elevation + height);
|
|
};
|
|
|
|
|
|
/**
|
|
* Convert room.points (relative coordinates) to absolute coordinates.
|
|
*
|
|
* @param {object} room The room
|
|
*
|
|
* @return {Array} Array of absolute coordinate points {x, y}
|
|
*/
|
|
beestat.component.scene.prototype.convert_room_to_absolute_polygon_ = function(room) {
|
|
const absolute = [];
|
|
room.points.forEach(function(point) {
|
|
absolute.push({
|
|
'x': room.x + point.x,
|
|
'y': room.y + point.y
|
|
});
|
|
});
|
|
return absolute;
|
|
};
|
|
|
|
|
|
/**
|
|
* Compute which ceiling areas are exposed (not covered by floors above).
|
|
*
|
|
* @param {object} floor_plan The floor plan
|
|
*
|
|
* @return {Array} Array of {ceiling_z, polygons[]} for roof outline rendering
|
|
*/
|
|
beestat.component.scene.prototype.compute_exposed_ceiling_areas_ = function(floor_plan) {
|
|
const self = this;
|
|
|
|
// Step 1: Group ceilings by Z-level
|
|
const ceiling_levels = {}; // Key: ceiling_z, Value: array of room polygons
|
|
|
|
floor_plan.data.groups.forEach(function(group) {
|
|
group.rooms.forEach(function(room) {
|
|
const elevation = room.elevation || group.elevation || 0;
|
|
|
|
// Skip basements (below ground)
|
|
if (elevation < 0) {
|
|
return;
|
|
}
|
|
|
|
const ceiling_z = self.get_ceiling_z_(group, room);
|
|
|
|
if (!ceiling_levels[ceiling_z]) {
|
|
ceiling_levels[ceiling_z] = [];
|
|
}
|
|
|
|
ceiling_levels[ceiling_z].push(
|
|
self.convert_room_to_absolute_polygon_(room)
|
|
);
|
|
});
|
|
});
|
|
|
|
// Step 2: Sort ceiling levels (ascending Z = highest to lowest)
|
|
const sorted_levels = Object.keys(ceiling_levels)
|
|
.map(z => parseFloat(z))
|
|
.sort((a, b) => a - b);
|
|
|
|
const exposed_areas = [];
|
|
|
|
// Step 3: For each level, compute exposed area
|
|
sorted_levels.forEach(function(current_ceiling_z, index) {
|
|
const current_polygons = ceiling_levels[current_ceiling_z];
|
|
|
|
// Union all rooms at this level
|
|
const union_clipper = new ClipperLib.Clipper();
|
|
current_polygons.forEach(function(polygon) {
|
|
union_clipper.AddPath(polygon, ClipperLib.PolyType.ptSubject, true);
|
|
});
|
|
|
|
const ceiling_area = new ClipperLib.Paths();
|
|
union_clipper.Execute(
|
|
ClipperLib.ClipType.ctUnion,
|
|
ceiling_area,
|
|
ClipperLib.PolyFillType.pftNonZero,
|
|
ClipperLib.PolyFillType.pftNonZero
|
|
);
|
|
|
|
// Compute occlusion from all higher levels
|
|
const occlusion_clipper = new ClipperLib.Clipper();
|
|
let has_occlusion = false;
|
|
|
|
for (let i = 0; i < index; i++) {
|
|
const above_ceiling_z = sorted_levels[i];
|
|
const above_polygons = ceiling_levels[above_ceiling_z];
|
|
|
|
above_polygons.forEach(function(polygon) {
|
|
occlusion_clipper.AddPath(polygon, ClipperLib.PolyType.ptSubject, true);
|
|
has_occlusion = true;
|
|
});
|
|
}
|
|
|
|
let exposed;
|
|
|
|
if (!has_occlusion) {
|
|
// Top floor - no occlusion, entire ceiling is exposed
|
|
exposed = ceiling_area;
|
|
} else {
|
|
// Compute union of all occlusion polygons
|
|
const occlusion_area = new ClipperLib.Paths();
|
|
occlusion_clipper.Execute(
|
|
ClipperLib.ClipType.ctUnion,
|
|
occlusion_area,
|
|
ClipperLib.PolyFillType.pftNonZero,
|
|
ClipperLib.PolyFillType.pftNonZero
|
|
);
|
|
|
|
// Subtract occlusion from ceiling
|
|
const diff_clipper = new ClipperLib.Clipper();
|
|
ceiling_area.forEach(function(path) {
|
|
diff_clipper.AddPath(path, ClipperLib.PolyType.ptSubject, true);
|
|
});
|
|
occlusion_area.forEach(function(path) {
|
|
diff_clipper.AddPath(path, ClipperLib.PolyType.ptClip, true);
|
|
});
|
|
|
|
exposed = new ClipperLib.Paths();
|
|
diff_clipper.Execute(
|
|
ClipperLib.ClipType.ctDifference,
|
|
exposed,
|
|
ClipperLib.PolyFillType.pftNonZero,
|
|
ClipperLib.PolyFillType.pftNonZero
|
|
);
|
|
}
|
|
|
|
// Filter out tiny polygons (floating-point artifacts)
|
|
const filtered = exposed.filter(function(path) {
|
|
return Math.abs(ClipperLib.Clipper.Area(path)) > 1;
|
|
});
|
|
|
|
if (filtered.length > 0) {
|
|
exposed_areas.push({
|
|
'ceiling_z': current_ceiling_z,
|
|
'polygons': filtered
|
|
});
|
|
}
|
|
});
|
|
|
|
return exposed_areas;
|
|
};
|
|
|
|
|
|
/**
|
|
* Return whether the current scene date is Christmas Day (12/25).
|
|
*
|
|
* @return {boolean}
|
|
*/
|
|
beestat.component.scene.prototype.is_christmas_day_ = function() {
|
|
if (
|
|
this.date_ === undefined ||
|
|
typeof this.date_.month !== 'function' ||
|
|
typeof this.date_.date !== 'function'
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return this.date_.month() === 11 && this.date_.date() === 25;
|
|
};
|
|
|
|
|
|
/**
|
|
* Toggle Christmas roofline lights visibility based on active date.
|
|
*/
|
|
beestat.component.scene.prototype.update_christmas_lights_visibility_ = function() {
|
|
if (this.christmas_lights_group_ === undefined) {
|
|
return;
|
|
}
|
|
|
|
this.christmas_lights_group_.visible = this.is_christmas_day_();
|
|
};
|
|
|
|
|
|
/**
|
|
* Add cheap Christmas roofline bulbs along resolved roof perimeter edges.
|
|
*
|
|
* @param {THREE.Group} roofs_layer
|
|
* @param {Array<{z: number, polygon: Array<{x:number,y:number}>}>} roofline_paths
|
|
*/
|
|
beestat.component.scene.prototype.add_christmas_lights_ = function(roofs_layer, roofline_paths) {
|
|
if (
|
|
roofs_layer === undefined ||
|
|
Array.isArray(roofline_paths) !== true ||
|
|
roofline_paths.length === 0
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const spacing = Math.max(8, Number(beestat.component.scene.christmas_light_spacing || 28));
|
|
const bulb_radius = Math.max(1, Number(beestat.component.scene.christmas_light_size || 4.2));
|
|
const colors = Array.isArray(beestat.component.scene.christmas_light_colors) === true &&
|
|
beestat.component.scene.christmas_light_colors.length > 0
|
|
? beestat.component.scene.christmas_light_colors
|
|
: [0xff2b2b, 0x34d44e, 0x3f7bff, 0xffd93a];
|
|
const bulb_z_offset = 2.5;
|
|
|
|
const positions_by_color = {};
|
|
colors.forEach(function(color) {
|
|
positions_by_color[color] = [];
|
|
});
|
|
|
|
let bulb_index = 0;
|
|
roofline_paths.forEach(function(path) {
|
|
const polygon = path.polygon;
|
|
if (Array.isArray(polygon) !== true || polygon.length < 2) {
|
|
return;
|
|
}
|
|
|
|
for (let i = 0; i < polygon.length; i++) {
|
|
const a = polygon[i];
|
|
const b = polygon[(i + 1) % polygon.length];
|
|
const dx = b.x - a.x;
|
|
const dy = b.y - a.y;
|
|
const length = Math.sqrt((dx * dx) + (dy * dy));
|
|
const count = Math.max(1, Math.round(length / spacing));
|
|
|
|
for (let j = 0; j < count; j++) {
|
|
const t = j / count;
|
|
const x = a.x + (dx * t);
|
|
const y = a.y + (dy * t);
|
|
const z = path.z - bulb_z_offset;
|
|
const color = colors[bulb_index % colors.length];
|
|
positions_by_color[color].push({'x': x, 'y': y, 'z': z});
|
|
bulb_index++;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (bulb_index === 0) {
|
|
return;
|
|
}
|
|
|
|
const christmas_group = new THREE.Group();
|
|
christmas_group.userData.is_roof_christmas_lights = true;
|
|
christmas_group.layers.set(beestat.component.scene.layer_visible);
|
|
|
|
const matrix = new THREE.Matrix4();
|
|
const bulb_geometry = new THREE.SphereGeometry(bulb_radius, 6, 5);
|
|
colors.forEach(function(color) {
|
|
const positions = positions_by_color[color];
|
|
if (Array.isArray(positions) !== true || positions.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const material = new THREE.MeshBasicMaterial({
|
|
'color': color,
|
|
'toneMapped': false
|
|
});
|
|
const bulbs = new THREE.InstancedMesh(bulb_geometry, material, positions.length);
|
|
bulbs.castShadow = false;
|
|
bulbs.receiveShadow = false;
|
|
bulbs.layers.set(beestat.component.scene.layer_visible);
|
|
|
|
for (let i = 0; i < positions.length; i++) {
|
|
const position = positions[i];
|
|
matrix.makeTranslation(position.x, position.y, position.z);
|
|
bulbs.setMatrixAt(i, matrix);
|
|
}
|
|
bulbs.instanceMatrix.needsUpdate = true;
|
|
christmas_group.add(bulbs);
|
|
});
|
|
|
|
roofs_layer.add(christmas_group);
|
|
this.christmas_lights_group_ = christmas_group;
|
|
this.update_christmas_lights_visibility_();
|
|
};
|
|
|
|
|
|
/**
|
|
* Add roofs to the scene based on the configured roof style.
|
|
*/
|
|
beestat.component.scene.prototype.add_roofs_ = function() {
|
|
// Resolve configured roof mode and available skeleton runtime.
|
|
const skeleton_builder = this.get_skeleton_builder_();
|
|
const roof_style = this.get_appearance_value_('roof_style');
|
|
|
|
// Prefer requested roof style; fall back to flat until skeleton runtime is ready.
|
|
if (roof_style === 'flat') {
|
|
this.add_flat_roofs_();
|
|
} else if (roof_style === 'hip' && skeleton_builder !== undefined) {
|
|
this.add_hip_roofs_(skeleton_builder);
|
|
} else {
|
|
if (roof_style === 'hip') {
|
|
this.listen_for_skeleton_builder_ready_();
|
|
}
|
|
this.add_flat_roofs_();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Add hip roofs using the straight skeleton algorithm.
|
|
*
|
|
* @param {object} skeleton_builder
|
|
*/
|
|
beestat.component.scene.prototype.add_hip_roofs_ = function(skeleton_builder) {
|
|
// Gather exposed ceiling polygons and shared roof style settings.
|
|
const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_];
|
|
const exposed_areas = this.compute_exposed_ceiling_areas_(floor_plan);
|
|
const roof_color = this.get_appearance_value_('roof_color');
|
|
|
|
// Create layer for generated roof meshes.
|
|
const roofs_layer = new THREE.Group();
|
|
this.floor_plan_group_.add(roofs_layer);
|
|
this.layers_['roof'] = roofs_layer;
|
|
|
|
const roof_pitch = beestat.component.scene.roof_pitch;
|
|
const roofline_paths = [];
|
|
|
|
// Build hip roof geometry per exposed polygon.
|
|
exposed_areas.forEach(function(area) {
|
|
area.polygons.forEach(function(polygon) {
|
|
if (polygon.length < 3) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Normalize polygon topology before offset/skeleton operations.
|
|
const simplified = ClipperLib.Clipper.SimplifyPolygon(
|
|
polygon,
|
|
ClipperLib.PolyFillType.pftNonZero
|
|
);
|
|
|
|
simplified.forEach(function(simple_polygon) {
|
|
if (simple_polygon.length < 3) {
|
|
return;
|
|
}
|
|
|
|
// Expand polygon to add overhang around the exposed ceiling footprint.
|
|
const roof_overhang = beestat.component.scene.roof_overhang;
|
|
const clipper_offset = new ClipperLib.ClipperOffset();
|
|
clipper_offset.AddPath(
|
|
simple_polygon,
|
|
ClipperLib.JoinType.jtMiter,
|
|
ClipperLib.EndType.etClosedPolygon
|
|
);
|
|
const offset_polygons = new ClipperLib.Paths();
|
|
clipper_offset.Execute(offset_polygons, roof_overhang);
|
|
|
|
// Use the offset polygon if successful, otherwise use original
|
|
const roof_polygon = (offset_polygons.length > 0) ? offset_polygons[0] : simple_polygon;
|
|
roofline_paths.push({
|
|
'z': area.ceiling_z,
|
|
'polygon': roof_polygon
|
|
});
|
|
|
|
// Add a thin base skirt so eaves have subtle physical thickness.
|
|
const base_shape = new THREE.Shape();
|
|
base_shape.moveTo(roof_polygon[0].x, roof_polygon[0].y);
|
|
for (let i = 1; i < roof_polygon.length; i++) {
|
|
base_shape.lineTo(roof_polygon[i].x, roof_polygon[i].y);
|
|
}
|
|
base_shape.closePath();
|
|
|
|
const hip_roof_base_thickness = 4;
|
|
const base_geometry = new THREE.ExtrudeGeometry(base_shape, {
|
|
'depth': hip_roof_base_thickness,
|
|
'bevelEnabled': false
|
|
});
|
|
const base_material = new THREE.MeshStandardMaterial({
|
|
'color': roof_color,
|
|
'side': THREE.DoubleSide,
|
|
'flatShading': false,
|
|
'roughness': 0.85,
|
|
'metalness': 0.0
|
|
});
|
|
const base_mesh = new THREE.Mesh(base_geometry, base_material);
|
|
// Nudge downward so the top cap doesn't z-fight with hip roof faces.
|
|
base_mesh.position.z = area.ceiling_z + 0.5;
|
|
base_mesh.userData.is_roof = true;
|
|
base_mesh.layers.set(beestat.component.scene.layer_visible);
|
|
base_mesh.castShadow = true;
|
|
base_mesh.receiveShadow = true;
|
|
roofs_layer.add(base_mesh);
|
|
|
|
// Convert polygon into straight-skeleton input format.
|
|
const ring = roof_polygon.map(function(point) {
|
|
return [point.x, point.y];
|
|
});
|
|
ring.push([roof_polygon[0].x, roof_polygon[0].y]);
|
|
|
|
const coordinates = [ring];
|
|
const result = skeleton_builder.buildFromPolygon(coordinates);
|
|
|
|
if (!result) {
|
|
return;
|
|
}
|
|
|
|
// Boundary vertices stay at ceiling level; interior vertices get raised by pitch.
|
|
const boundary_vertex_count = roof_polygon.length;
|
|
const boundary_set = new Set();
|
|
for (let i = 0; i < boundary_vertex_count; i++) {
|
|
boundary_set.add(i);
|
|
}
|
|
|
|
// Helper: compute shortest distance from a point to roof footprint edges.
|
|
const compute_distance_to_boundary = function(point_x, point_y) {
|
|
let min_distance = Infinity;
|
|
|
|
for (let i = 0; i < roof_polygon.length; i++) {
|
|
const p1 = roof_polygon[i];
|
|
const p2 = roof_polygon[(i + 1) % roof_polygon.length];
|
|
|
|
// Calculate perpendicular distance from point to line segment
|
|
const dx = p2.x - p1.x;
|
|
const dy = p2.y - p1.y;
|
|
const length_sq = dx * dx + dy * dy;
|
|
|
|
if (length_sq === 0) {
|
|
// Point to point distance
|
|
const dist = Math.sqrt(
|
|
Math.pow(point_x - p1.x, 2) + Math.pow(point_y - p1.y, 2)
|
|
);
|
|
min_distance = Math.min(min_distance, dist);
|
|
continue;
|
|
}
|
|
|
|
// Project point onto line segment
|
|
let t = ((point_x - p1.x) * dx + (point_y - p1.y) * dy) / length_sq;
|
|
t = Math.max(0, Math.min(1, t));
|
|
|
|
const closest_x = p1.x + t * dx;
|
|
const closest_y = p1.y + t * dy;
|
|
|
|
const dist = Math.sqrt(
|
|
Math.pow(point_x - closest_x, 2) + Math.pow(point_y - closest_y, 2)
|
|
);
|
|
|
|
min_distance = Math.min(min_distance, dist);
|
|
}
|
|
|
|
return min_distance;
|
|
};
|
|
|
|
// Lift interior skeleton vertices to form sloped hip planes.
|
|
const vertices_3d = result.vertices.map(function(vertex, index) {
|
|
const is_boundary = boundary_set.has(index);
|
|
let height = 0;
|
|
|
|
if (!is_boundary) {
|
|
// Interior skeleton vertex - raise it based on distance to boundary
|
|
const distance = compute_distance_to_boundary(vertex[0], vertex[1]);
|
|
height = distance * roof_pitch;
|
|
}
|
|
|
|
return new THREE.Vector3(
|
|
vertex[0],
|
|
vertex[1],
|
|
area.ceiling_z - height // Negative Z = higher in world coords
|
|
);
|
|
});
|
|
|
|
// Triangulate each skeleton face and emit a renderable mesh.
|
|
result.polygons.forEach(function(face) {
|
|
if (face.length < 3) {
|
|
return;
|
|
}
|
|
|
|
// Create triangulated mesh for this face
|
|
const face_vertices = face.map(function(idx) {
|
|
return vertices_3d[idx];
|
|
});
|
|
|
|
// Triangulate the face (simple fan triangulation from first vertex)
|
|
const triangles = [];
|
|
for (let i = 1; i < face_vertices.length - 1; i++) {
|
|
triangles.push(
|
|
face_vertices[0],
|
|
face_vertices[i],
|
|
face_vertices[i + 1]
|
|
);
|
|
}
|
|
|
|
// Create geometry
|
|
const geometry = new THREE.BufferGeometry().setFromPoints(triangles);
|
|
geometry.computeVertexNormals();
|
|
|
|
// Create material - use appearance roof color
|
|
const material = new THREE.MeshStandardMaterial({
|
|
'color': roof_color,
|
|
'side': THREE.DoubleSide,
|
|
'flatShading': false,
|
|
'roughness': 0.8,
|
|
'metalness': 0.0
|
|
});
|
|
|
|
const mesh = new THREE.Mesh(geometry, material);
|
|
mesh.userData.is_roof = true;
|
|
mesh.layers.set(beestat.component.scene.layer_visible);
|
|
mesh.castShadow = true;
|
|
mesh.receiveShadow = true;
|
|
roofs_layer.add(mesh);
|
|
});
|
|
});
|
|
} catch (error) {
|
|
console.error('Error generating roof:', error, polygon);
|
|
}
|
|
});
|
|
});
|
|
|
|
this.add_christmas_lights_(roofs_layer, roofline_paths);
|
|
};
|
|
|
|
|
|
/**
|
|
* Add flat roofs to the scene.
|
|
*/
|
|
beestat.component.scene.prototype.add_flat_roofs_ = function() {
|
|
// Gather exposed ceiling polygons and shared roof style settings.
|
|
const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_];
|
|
const exposed_areas = this.compute_exposed_ceiling_areas_(floor_plan);
|
|
const roof_color = this.get_appearance_value_('roof_color');
|
|
|
|
// Create layer for generated roof meshes.
|
|
const roofs_layer = new THREE.Group();
|
|
this.floor_plan_group_.add(roofs_layer);
|
|
this.layers_['roof'] = roofs_layer;
|
|
const roofline_paths = [];
|
|
|
|
// Build flat roof geometry per exposed polygon.
|
|
exposed_areas.forEach(function(area) {
|
|
area.polygons.forEach(function(polygon) {
|
|
if (polygon.length < 3) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Normalize polygon topology before offset/extrusion.
|
|
const simplified = ClipperLib.Clipper.SimplifyPolygon(
|
|
polygon,
|
|
ClipperLib.PolyFillType.pftNonZero
|
|
);
|
|
|
|
simplified.forEach(function(simple_polygon) {
|
|
if (simple_polygon.length < 3) {
|
|
return;
|
|
}
|
|
|
|
// Expand polygon to add overhang around the exposed ceiling footprint.
|
|
const roof_overhang = beestat.component.scene.roof_overhang;
|
|
const clipper_offset = new ClipperLib.ClipperOffset();
|
|
clipper_offset.AddPath(
|
|
simple_polygon,
|
|
ClipperLib.JoinType.jtMiter,
|
|
ClipperLib.EndType.etClosedPolygon
|
|
);
|
|
const offset_polygons = new ClipperLib.Paths();
|
|
clipper_offset.Execute(offset_polygons, roof_overhang);
|
|
|
|
// Use the offset polygon if successful, otherwise use original
|
|
const roof_polygon = (offset_polygons.length > 0) ? offset_polygons[0] : simple_polygon;
|
|
roofline_paths.push({
|
|
'z': area.ceiling_z,
|
|
'polygon': roof_polygon
|
|
});
|
|
|
|
// Build the flat roof footprint shape for extrusion.
|
|
const shape = new THREE.Shape();
|
|
shape.moveTo(roof_polygon[0].x, roof_polygon[0].y);
|
|
for (let i = 1; i < roof_polygon.length; i++) {
|
|
shape.lineTo(roof_polygon[i].x, roof_polygon[i].y);
|
|
}
|
|
shape.closePath();
|
|
|
|
// Extrude the footprint so the flat roof has physical depth.
|
|
const flat_roof_depth = 6; // 6 inches of depth
|
|
const geometry = new THREE.ExtrudeGeometry(shape, {
|
|
'depth': flat_roof_depth,
|
|
'bevelEnabled': false
|
|
});
|
|
|
|
// Create material - use appearance roof color
|
|
const material = new THREE.MeshStandardMaterial({
|
|
'color': roof_color,
|
|
'side': THREE.DoubleSide,
|
|
'flatShading': false,
|
|
'roughness': 0.9, // Slightly higher roughness for flat roofs
|
|
'metalness': 0.0
|
|
});
|
|
|
|
const mesh = new THREE.Mesh(geometry, material);
|
|
mesh.position.z = area.ceiling_z - flat_roof_depth; // Position so top is at ceiling level
|
|
mesh.userData.is_roof = true;
|
|
mesh.layers.set(beestat.component.scene.layer_visible);
|
|
mesh.castShadow = true;
|
|
mesh.receiveShadow = true;
|
|
|
|
roofs_layer.add(mesh);
|
|
});
|
|
} catch (error) {
|
|
console.error('Error generating flat roof:', error, polygon);
|
|
}
|
|
});
|
|
});
|
|
|
|
this.add_christmas_lights_(roofs_layer, roofline_paths);
|
|
};
|
|
|
|
|
|
/**
|
|
* Get the straight-skeleton runtime when it has finished initializing.
|
|
*
|
|
* @return {object|undefined}
|
|
*/
|
|
beestat.component.scene.prototype.get_skeleton_builder_ = function() {
|
|
if (window.SkeletonBuilderInitialized === true) {
|
|
return window.SkeletonBuilder;
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
|
|
/**
|
|
* If the skeleton runtime is still loading, listen for readiness and rerender
|
|
* once so hip roofs replace fallback flat roofs.
|
|
*/
|
|
beestat.component.scene.prototype.listen_for_skeleton_builder_ready_ = function() {
|
|
const self = this;
|
|
|
|
if (this.skeleton_builder_ready_handler_ !== undefined) {
|
|
return;
|
|
}
|
|
|
|
this.skeleton_builder_ready_handler_ = function() {
|
|
if (self.skeleton_builder_ready_handler_ !== undefined) {
|
|
window.removeEventListener('skeleton_builder_ready', self.skeleton_builder_ready_handler_);
|
|
delete self.skeleton_builder_ready_handler_;
|
|
}
|
|
|
|
if (self.rendered_ === true) {
|
|
self.rerender();
|
|
}
|
|
};
|
|
|
|
window.addEventListener('skeleton_builder_ready', this.skeleton_builder_ready_handler_);
|
|
};
|