1use super::*;
20
21#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
23pub struct SlabPlate {
24 #[serde(default)]
33 pub section: Option<crate::ids::SectionId>,
34 #[serde(default)]
36 pub loads: Vec<AreaLoad>,
37 #[serde(default)]
40 pub usage: Option<SlabUsage>,
41 pub method: DistributionMethod,
43 #[serde(default)]
45 pub one_way: Option<OneWayDir>,
46}
47
48impl SlabPlate {
49 pub fn finish_intensity(&self) -> f64 {
51 self.loads.iter().map(|l| l.value).sum()
52 }
53
54 pub fn live_intensity(&self, purpose: LoadPurpose) -> f64 {
56 self.usage.map(|u| u.live_load(purpose)).unwrap_or(0.0)
57 }
58}
59
60#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
62pub enum SlabShape {
63 Enclosed { boundary: Vec<NodeId> },
65 Attached {
67 anchor: RegionAnchor,
69 extent: [f64; 2],
71 },
72}
73
74#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
77pub struct Slab {
78 pub id: SlabId,
80 pub shape: SlabShape,
81 pub plate: SlabPlate,
82}
83
84impl Slab {
85 pub fn is_attached(&self) -> bool {
87 matches!(self.shape, SlabShape::Attached { .. })
88 }
89
90 pub fn boundary_nodes(&self) -> Option<&[NodeId]> {
93 match &self.shape {
94 SlabShape::Enclosed { boundary } => Some(boundary),
95 SlabShape::Attached { .. } => None,
96 }
97 }
98
99 pub fn boundary_coords(&self, model: &Model) -> Option<Vec<[f64; 3]>> {
102 self.boundary_coords_with(|n| model.nodes.get(n.index()).map(|n| n.coord))
103 }
104
105 pub fn boundary_coords_with(
112 &self,
113 coord_of: impl Fn(NodeId) -> Option<[f64; 3]>,
114 ) -> Option<Vec<[f64; 3]>> {
115 match &self.shape {
116 SlabShape::Enclosed { boundary } => boundary.iter().map(|n| coord_of(*n)).collect(),
117 SlabShape::Attached { anchor, extent } => match anchor {
118 RegionAnchor::Line { nodes, span, .. } => {
119 let a = coord_of(nodes[0])?;
120 let b = coord_of(nodes[1])?;
121 let lerp = |t: f64| {
122 [
123 a[0] + (b[0] - a[0]) * t,
124 a[1] + (b[1] - a[1]) * t,
125 a[2] + (b[2] - a[2]) * t,
126 ]
127 };
128 let p0 = lerp(span[0]);
129 let p1 = lerp(span[1]);
130 let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
131 let len = (dx * dx + dy * dy).sqrt();
132 if len <= f64::EPSILON {
133 return None;
134 }
135 let n = [-dy / len, dx / len];
137 Some(vec![
138 p0,
139 p1,
140 [p1[0] + n[0] * extent[1], p1[1] + n[1] * extent[1], p1[2]],
141 [p0[0] + n[0] * extent[0], p0[1] + n[1] * extent[0], p0[2]],
142 ])
143 }
144 RegionAnchor::Point(nid) => {
145 let p = coord_of(*nid)?;
146 Some(vec![
147 p,
148 [p[0] + extent[0], p[1], p[2]],
149 [p[0] + extent[0], p[1] + extent[1], p[2]],
150 [p[0], p[1] + extent[1], p[2]],
151 ])
152 }
153 RegionAnchor::FloorRegion { .. } => None,
157 },
158 }
159 }
160
161 pub fn edge_nodes(&self, k: usize) -> Option<[NodeId; 2]> {
168 match &self.shape {
169 SlabShape::Enclosed { boundary } => {
170 let n = boundary.len();
171 (n >= 3 && k < n).then(|| [boundary[k], boundary[(k + 1) % n]])
172 }
173 SlabShape::Attached { anchor, .. } => match anchor {
174 RegionAnchor::Line { nodes, .. } if k == 0 => Some(*nodes),
175 RegionAnchor::Line { .. } | RegionAnchor::Point(_) => None,
176 RegionAnchor::FloorRegion { .. } => None,
178 },
179 }
180 }
181
182 pub fn reference_node(&self) -> Option<NodeId> {
185 match &self.shape {
186 SlabShape::Enclosed { boundary } => boundary.first().copied(),
187 SlabShape::Attached { anchor, .. } => match anchor {
188 RegionAnchor::Line { nodes, .. } => Some(nodes[0]),
189 RegionAnchor::Point(n) => Some(*n),
190 RegionAnchor::FloorRegion { .. } => None,
192 },
193 }
194 }
195
196 pub fn level(&self, model: &Model) -> Option<f64> {
198 let coords = self.boundary_coords(model)?;
199 if coords.is_empty() {
200 return None;
201 }
202 Some(coords.iter().map(|c| c[2]).sum::<f64>() / coords.len() as f64)
203 }
204
205 pub fn method(&self) -> DistributionMethod {
207 self.plate.method
208 }
209
210 pub fn one_way(&self) -> Option<OneWayDir> {
212 self.plate.one_way
213 }
214
215 pub fn live_intensity(&self, purpose: LoadPurpose) -> f64 {
217 self.plate.live_intensity(purpose)
218 }
219
220 pub fn section(&self) -> Option<crate::ids::SectionId> {
222 self.plate.section
223 }
224
225 pub fn usage(&self) -> Option<SlabUsage> {
227 self.plate.usage
228 }
229
230 pub fn attached_design_span(&self) -> Option<f64> {
233 match &self.shape {
234 SlabShape::Enclosed { .. } => None,
235 SlabShape::Attached { extent, .. } => {
236 let span = extent[0].abs().max(extent[1].abs());
237 (span.is_finite() && span > 0.0).then_some(span)
238 }
239 }
240 }
241}
242
243impl Model {
244 pub fn slab(&self, id: SlabId) -> Option<&Slab> {
246 match self.slabs.get(id.index()) {
247 Some(s) if s.id == id => Some(s),
248 _ => self.slabs.iter().find(|s| s.id == id),
249 }
250 }
251
252 pub fn slab_section(&self, slab: &Slab) -> Option<&Section> {
254 slab.section()
255 .and_then(|sid| self.sections.get(sid.index()))
256 }
257
258 pub fn slab_plate_thickness(&self, slab: &Slab) -> Option<f64> {
265 self.slab_section(slab)
266 .and_then(|s| s.thickness)
267 .filter(|t| *t > 0.0)
268 }
269
270 pub fn slab_self_weight_intensity(&self, slab: &Slab) -> Option<f64> {
276 let t = self.slab_plate_thickness(slab)?;
277 let mat = self
278 .slab_section(slab)
279 .and_then(|s| s.material)
280 .and_then(|mid| self.materials.get(mid.index()))?;
281 Some(t * mat.density * crate::units::GRAVITY_MM_S2)
282 }
283
284 pub fn slab_dead_intensity(&self, slab: &Slab) -> f64 {
289 self.slab_self_weight_intensity(slab).unwrap_or(0.0) + slab.plate.finish_intensity()
290 }
291
292 pub fn slab_intensity(&self, slab: &Slab, purpose: LoadPurpose) -> f64 {
295 self.slab_dead_intensity(slab) + slab.plate.live_intensity(purpose)
296 }
297}
298
299#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
301pub enum DistributionMethod {
302 #[default]
303 TriTrapezoid,
304 OneWay,
305 TributaryArea,
306}
307
308#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
313pub enum LoadPurpose {
314 Floor,
315 Frame,
316 Seismic,
317}
318
319#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
328pub enum SlabUsage {
329 Residential,
331 Office,
333 ResearchRoom,
335 Classroom,
337 Store,
339 AssemblyFixed,
341 AssemblyOther,
343 Corridor,
345 RegistryArchive,
347 GeneralArchive,
349 MobileArchive,
351 LabChemistry,
353 LabPhysics,
355 ComputerRoom,
357 MachineRoom,
359 Gymnasium,
361 Garage,
363 Balcony,
365 RoofResidential,
367 RoofStore,
369 RoofUnused,
371 RoofSteelGym,
374 Custom {
376 floor: f64,
377 frame: f64,
378 seismic: f64,
379 },
380}
381
382impl SlabUsage {
383 pub fn live_load(self, purpose: LoadPurpose) -> f64 {
385 let (floor, frame, seismic) = match self {
388 SlabUsage::Residential => (1800.0, 1300.0, 600.0),
389 SlabUsage::Office => (2900.0, 1800.0, 800.0),
390 SlabUsage::ResearchRoom => (2900.0, 1800.0, 800.0),
391 SlabUsage::Classroom => (2300.0, 2100.0, 1100.0),
392 SlabUsage::Store => (2900.0, 2400.0, 1300.0),
393 SlabUsage::AssemblyFixed => (2900.0, 2600.0, 1600.0),
394 SlabUsage::AssemblyOther => (3500.0, 3200.0, 2100.0),
395 SlabUsage::Corridor => (3500.0, 3200.0, 2100.0),
396 SlabUsage::RegistryArchive => (5900.0, 4900.0, 3900.0),
397 SlabUsage::GeneralArchive => (7800.0, 6900.0, 4900.0),
398 SlabUsage::MobileArchive => (11800.0, 10300.0, 7400.0),
399 SlabUsage::LabChemistry => (3900.0, 2400.0, 1600.0),
400 SlabUsage::LabPhysics => (4900.0, 3900.0, 2500.0),
401 SlabUsage::ComputerRoom => (4900.0, 2400.0, 1300.0),
402 SlabUsage::MachineRoom => (4900.0, 2400.0, 1300.0),
403 SlabUsage::Gymnasium => (3500.0, 3200.0, 2100.0),
404 SlabUsage::Garage => (5400.0, 3900.0, 2000.0),
405 SlabUsage::Balcony => (1800.0, 1300.0, 600.0),
406 SlabUsage::RoofResidential => (1800.0, 1300.0, 600.0),
407 SlabUsage::RoofStore => (2900.0, 2400.0, 1300.0),
408 SlabUsage::RoofUnused => (980.0, 600.0, 400.0),
409 SlabUsage::RoofSteelGym => (980.0, 0.0, 0.0),
410 SlabUsage::Custom {
412 floor,
413 frame,
414 seismic,
415 } => {
416 return match purpose {
417 LoadPurpose::Floor => floor,
418 LoadPurpose::Frame => frame,
419 LoadPurpose::Seismic => seismic,
420 };
421 }
422 };
423 let v_n_per_m2 = match purpose {
424 LoadPurpose::Floor => floor,
425 LoadPurpose::Frame => frame,
426 LoadPurpose::Seismic => seismic,
427 };
428 v_n_per_m2 * 1e-6
429 }
430}
431
432#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
433pub struct AreaLoad {
434 pub kind: String,
435 pub value: f64,
436}
437
438#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
441pub enum OneWayDir {
442 X,
443 Y,
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449
450 #[test]
451 fn test_usage_table_values_n_per_mm2() {
452 let o = SlabUsage::Office;
454 assert!((o.live_load(LoadPurpose::Floor) - 2900e-6).abs() < 1e-12);
455 assert!((o.live_load(LoadPurpose::Frame) - 1800e-6).abs() < 1e-12);
456 assert!((o.live_load(LoadPurpose::Seismic) - 800e-6).abs() < 1e-12);
457 let r = SlabUsage::Residential;
459 assert!((r.live_load(LoadPurpose::Floor) - 1800e-6).abs() < 1e-12);
460 assert!((r.live_load(LoadPurpose::Frame) - 1300e-6).abs() < 1e-12);
461 assert!((r.live_load(LoadPurpose::Seismic) - 600e-6).abs() < 1e-12);
462 let cases: &[(SlabUsage, f64, f64, f64)] = &[
464 (SlabUsage::ResearchRoom, 2900.0, 1800.0, 800.0),
465 (SlabUsage::RegistryArchive, 5900.0, 4900.0, 3900.0),
466 (SlabUsage::GeneralArchive, 7800.0, 6900.0, 4900.0),
467 (SlabUsage::MobileArchive, 11800.0, 10300.0, 7400.0),
468 (SlabUsage::LabChemistry, 3900.0, 2400.0, 1600.0),
469 (SlabUsage::LabPhysics, 4900.0, 3900.0, 2500.0),
470 (SlabUsage::ComputerRoom, 4900.0, 2400.0, 1300.0),
471 (SlabUsage::MachineRoom, 4900.0, 2400.0, 1300.0),
472 (SlabUsage::Gymnasium, 3500.0, 3200.0, 2100.0),
473 (SlabUsage::Balcony, 1800.0, 1300.0, 600.0),
474 (SlabUsage::RoofUnused, 980.0, 600.0, 400.0),
475 (SlabUsage::RoofSteelGym, 980.0, 0.0, 0.0),
476 ];
477 for &(u, floor, frame, seismic) in cases {
478 assert!((u.live_load(LoadPurpose::Floor) - floor * 1e-6).abs() < 1e-12);
479 assert!((u.live_load(LoadPurpose::Frame) - frame * 1e-6).abs() < 1e-12);
480 assert!((u.live_load(LoadPurpose::Seismic) - seismic * 1e-6).abs() < 1e-12);
481 }
482
483 for u in [
485 SlabUsage::Residential,
486 SlabUsage::Office,
487 SlabUsage::ResearchRoom,
488 SlabUsage::Classroom,
489 SlabUsage::Store,
490 SlabUsage::AssemblyFixed,
491 SlabUsage::AssemblyOther,
492 SlabUsage::Corridor,
493 SlabUsage::RegistryArchive,
494 SlabUsage::GeneralArchive,
495 SlabUsage::MobileArchive,
496 SlabUsage::LabChemistry,
497 SlabUsage::LabPhysics,
498 SlabUsage::ComputerRoom,
499 SlabUsage::MachineRoom,
500 SlabUsage::Gymnasium,
501 SlabUsage::Garage,
502 SlabUsage::Balcony,
503 SlabUsage::RoofResidential,
504 SlabUsage::RoofStore,
505 SlabUsage::RoofUnused,
506 SlabUsage::RoofSteelGym,
507 ] {
508 let f = u.live_load(LoadPurpose::Floor);
509 let g = u.live_load(LoadPurpose::Frame);
510 let s = u.live_load(LoadPurpose::Seismic);
511 assert!(f >= g && g >= s, "床用≥骨組用≥地震用: {u:?}");
512 }
513 }
514
515 #[test]
516 fn test_usage_custom_is_internal_units() {
517 let c = SlabUsage::Custom {
519 floor: 3.0e-3,
520 frame: 2.0e-3,
521 seismic: 1.0e-3,
522 };
523 assert_eq!(c.live_load(LoadPurpose::Floor), 3.0e-3);
524 assert_eq!(c.live_load(LoadPurpose::Frame), 2.0e-3);
525 assert_eq!(c.live_load(LoadPurpose::Seismic), 1.0e-3);
526 }
527
528 #[test]
533 fn test_floor_region_anchor_never_yields_slab_geometry() {
534 let model = Model::default();
535 let slab = Slab {
536 id: crate::ids::SlabId(0),
537 shape: SlabShape::Attached {
538 anchor: RegionAnchor::FloorRegion {
539 nodes: [NodeId(0), NodeId(1)],
540 },
541 extent: [0.0, 0.0],
542 },
543 plate: SlabPlate::default(),
544 };
545 assert_eq!(slab.boundary_coords(&model), None);
546 assert_eq!(slab.reference_node(), None);
547 assert_eq!(slab.edge_nodes(0), None);
548 }
549}