440 lines
18 KiB
Rust
440 lines
18 KiB
Rust
|
|
// Auto-split from scene/mod.rs. Pure text-move; behaviour unchanged.
|
|||
|
|
use super::*;
|
|||
|
|
|
|||
|
|
impl Scene {
|
|||
|
|
/// Collect model-space wires projected into paper space for all (or one specific)
|
|||
|
|
/// user viewports. `only_vp = Some(h)` restricts output to that viewport.
|
|||
|
|
pub(super) fn viewport_content_wires(
|
|||
|
|
&self,
|
|||
|
|
paper_block: Handle,
|
|||
|
|
only_vp: Option<Handle>,
|
|||
|
|
exclude_vp: Option<Handle>,
|
|||
|
|
) -> Vec<WireModel> {
|
|||
|
|
use acadrust::entities::Viewport;
|
|||
|
|
|
|||
|
|
let viewports: Vec<&Viewport> = self
|
|||
|
|
.document
|
|||
|
|
.entities()
|
|||
|
|
.filter_map(|e| {
|
|||
|
|
if let EntityType::Viewport(vp) = e {
|
|||
|
|
Some(vp)
|
|||
|
|
} else {
|
|||
|
|
None
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
.filter(|vp| {
|
|||
|
|
self.is_content_viewport_in_layout(vp, paper_block)
|
|||
|
|
&& vp.status.is_on
|
|||
|
|
&& only_vp.map_or(true, |h| vp.common.handle == h)
|
|||
|
|
&& exclude_vp.map_or(true, |h| vp.common.handle != h)
|
|||
|
|
})
|
|||
|
|
.collect();
|
|||
|
|
|
|||
|
|
if viewports.is_empty() {
|
|||
|
|
return vec![];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let mut result = Vec::new();
|
|||
|
|
|
|||
|
|
for vp in viewports {
|
|||
|
|
let vp_handle = vp.common.handle;
|
|||
|
|
|
|||
|
|
// ── Fast path: return cached projected wires ──────────────────
|
|||
|
|
{
|
|||
|
|
let cache = self.paper_projected_cache.borrow();
|
|||
|
|
if let Some((cached_epoch, ref wires)) = cache.get(&vp_handle) {
|
|||
|
|
if *cached_epoch == self.geometry_epoch {
|
|||
|
|
result.extend_from_slice(wires);
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Cache miss: compute projection ────────────────────────────
|
|||
|
|
|
|||
|
|
// Use camera_for_viewport so the axes match the GPU renderer exactly.
|
|||
|
|
let cam_frame = match self.camera_for_viewport(vp_handle) {
|
|||
|
|
Some(c) => c,
|
|||
|
|
None => continue,
|
|||
|
|
};
|
|||
|
|
let view_right = cam_frame.rotation * glam::Vec3::X;
|
|||
|
|
let view_up = cam_frame.rotation * glam::Vec3::Y;
|
|||
|
|
|
|||
|
|
// Scale (paper units per model unit) comes straight from the camera
|
|||
|
|
// the GPU uses: the model height shown is `2 * ortho_size`, mapped
|
|||
|
|
// onto `vp.height` of paper. `camera_for_viewport` already made the
|
|||
|
|
// saved-view-vs-auto-fit decision (with the twist-correct overlap
|
|||
|
|
// test), so deriving scale from it keeps the CPU projection (used
|
|||
|
|
// for hit-test / snap / fit) locked to the GPU render — no second,
|
|||
|
|
// independently-computed scale that could disagree under a twist.
|
|||
|
|
let view_height_eff = (cam_frame.ortho_size() * 2.0) as f64;
|
|||
|
|
let scale = if view_height_eff > 1e-9 {
|
|||
|
|
(vp.height / view_height_eff) as f32
|
|||
|
|
} else {
|
|||
|
|
1.0
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let pcx = vp.center.x as f32;
|
|||
|
|
let pcy = vp.center.y as f32;
|
|||
|
|
let pcz = vp.center.z as f32;
|
|||
|
|
let hw = (vp.width / 2.0) as f32;
|
|||
|
|
let hh = (vp.height / 2.0) as f32;
|
|||
|
|
|
|||
|
|
// ── Use cached tessellation (model_wires_for_viewport_arc) ────
|
|||
|
|
// This eliminates the per-frame tessellate_one() loop that was here
|
|||
|
|
// previously; tessellation is now O(1) on navigation frames.
|
|||
|
|
// Pass 0.0 for screen height — the CPU-projection / hit-test
|
|||
|
|
// path wants the full-fidelity (no-LOD-stub) wire list,
|
|||
|
|
// regardless of paper zoom.
|
|||
|
|
let model_wires = self.model_wires_for_viewport_arc(vp_handle, 0.0);
|
|||
|
|
|
|||
|
|
// ── Project and clip wires into viewport ──────────────────────
|
|||
|
|
let vp_x0 = pcx - hw;
|
|||
|
|
let vp_x1 = pcx + hw;
|
|||
|
|
let vp_y0 = pcy - hh;
|
|||
|
|
let vp_y1 = pcy + hh;
|
|||
|
|
|
|||
|
|
// camera_dist: how far the camera is from the target plane.
|
|||
|
|
let use_perspective = vp.status.perspective && vp.lens_length > 1.0;
|
|||
|
|
let camera_dist = if use_perspective {
|
|||
|
|
(vp.view_height as f32 * vp.lens_length as f32 / 24.0).max(0.001)
|
|||
|
|
} else {
|
|||
|
|
0.0
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let mut projected: Vec<WireModel> = Vec::new();
|
|||
|
|
|
|||
|
|
// Precompute precision-stable WCS-space projection inputs in
|
|||
|
|
// f64. The previous f32 inner loop suffered catastrophic
|
|||
|
|
// cancellation on UTM-scale drawings: `(wire_offset_rel -
|
|||
|
|
// target_offset_rel).dot(view_right) - view_center` is a
|
|||
|
|
// small paper offset computed by subtracting two values at
|
|||
|
|
// ~5e6 magnitude — f32 ULP there is ~0.5 m, so paper output
|
|||
|
|
// jittered by cm even when the actual model was clean.
|
|||
|
|
//
|
|||
|
|
// Do everything WCS-relative in f64; cast to f32 only at the
|
|||
|
|
// final paper position.
|
|||
|
|
// Display centre = the camera's target, in WCS. `camera_for_viewport`
|
|||
|
|
// already folded view_center through the (twisted) view basis and
|
|||
|
|
// applied the empty-WCS auto-fit, so taking its target keeps the CPU
|
|||
|
|
// projection identical to the GPU renderer under any twist.
|
|||
|
|
let display_center_x = cam_frame.target.x as f64 + [0.0_f64; 3][0];
|
|||
|
|
let display_center_y = cam_frame.target.y as f64 + [0.0_f64; 3][1];
|
|||
|
|
let display_center_z = cam_frame.target.z as f64 + [0.0_f64; 3][2];
|
|||
|
|
let view_right_d = (
|
|||
|
|
view_right.x as f64,
|
|||
|
|
view_right.y as f64,
|
|||
|
|
view_right.z as f64,
|
|||
|
|
);
|
|||
|
|
let view_up_d = (view_up.x as f64, view_up.y as f64, view_up.z as f64);
|
|||
|
|
let view_fwd = cam_frame.rotation * glam::Vec3::Z;
|
|||
|
|
let view_fwd_d = (view_fwd.x as f64, view_fwd.y as f64, view_fwd.z as f64);
|
|||
|
|
let camera_dist_d = camera_dist as f64;
|
|||
|
|
let scale_d = scale as f64;
|
|||
|
|
let pcx_d = pcx as f64;
|
|||
|
|
let pcy_d = pcy as f64;
|
|||
|
|
// Project one ABSOLUTE-WCS model point (f64) onto the paper sheet.
|
|||
|
|
// Shared by the polyline points, snap points and key vertices so the
|
|||
|
|
// hit-test / snap geometry lands in the same paper frame the wire is
|
|||
|
|
// drawn in — otherwise snaps and the click-AABB stay in model (UTM)
|
|||
|
|
// space and the cursor never reaches them.
|
|||
|
|
let proj_abs = |ax: f64, ay: f64, az: f64| -> [f32; 3] {
|
|||
|
|
let mp_x = ax - display_center_x;
|
|||
|
|
let mp_y = ay - display_center_y;
|
|||
|
|
let mp_z = az - display_center_z;
|
|||
|
|
let u = mp_x * view_right_d.0 + mp_y * view_right_d.1 + mp_z * view_right_d.2;
|
|||
|
|
let v = mp_x * view_up_d.0 + mp_y * view_up_d.1 + mp_z * view_up_d.2;
|
|||
|
|
if use_perspective {
|
|||
|
|
let d_vd = mp_x * view_fwd_d.0 + mp_y * view_fwd_d.1 + mp_z * view_fwd_d.2;
|
|||
|
|
let fwd = camera_dist_d - d_vd;
|
|||
|
|
if fwd <= 0.001 {
|
|||
|
|
return [f32::NAN; 3];
|
|||
|
|
}
|
|||
|
|
let factor = camera_dist_d / fwd;
|
|||
|
|
[
|
|||
|
|
(pcx_d + u * factor * scale_d) as f32,
|
|||
|
|
(pcy_d + v * factor * scale_d) as f32,
|
|||
|
|
pcz,
|
|||
|
|
]
|
|||
|
|
} else {
|
|||
|
|
[(pcx_d + u * scale_d) as f32, (pcy_d + v * scale_d) as f32, pcz]
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
let in_vp = |x: f32, y: f32| x >= vp_x0 && x <= vp_x1 && y >= vp_y0 && y <= vp_y1;
|
|||
|
|
|
|||
|
|
for wire in model_wires.iter() {
|
|||
|
|
let projected_pts: Vec<[f32; 3]> = wire
|
|||
|
|
.points
|
|||
|
|
.iter()
|
|||
|
|
.enumerate()
|
|||
|
|
.map(|(pi, &[mx, my, mz])| {
|
|||
|
|
if mx.is_nan() || my.is_nan() || mz.is_nan() {
|
|||
|
|
return [f32::NAN; 3];
|
|||
|
|
}
|
|||
|
|
// Reconstruct absolute WCS from the double-single high
|
|||
|
|
// (`points`) + low (`points_low`) pair — the high f32
|
|||
|
|
// alone is ~0.5 m off at UTM scale.
|
|||
|
|
let lo = wire.points_low.get(pi).copied().unwrap_or([0.0; 3]);
|
|||
|
|
proj_abs(mx as f64 + lo[0] as f64, my as f64 + lo[1] as f64, mz as f64 + lo[2] as f64)
|
|||
|
|
})
|
|||
|
|
.collect();
|
|||
|
|
|
|||
|
|
// Fast AABB pre-reject.
|
|||
|
|
let any_near = projected_pts.iter().any(|&[x, y, _]| {
|
|||
|
|
x.is_finite()
|
|||
|
|
&& y.is_finite()
|
|||
|
|
&& x >= vp_x0 - 1.0
|
|||
|
|
&& x <= vp_x1 + 1.0
|
|||
|
|
&& y >= vp_y0 - 1.0
|
|||
|
|
&& y <= vp_y1 + 1.0
|
|||
|
|
});
|
|||
|
|
let (min_x, max_x, min_y, max_y) =
|
|||
|
|
projected_pts.iter().filter(|p| p[0].is_finite()).fold(
|
|||
|
|
(
|
|||
|
|
f32::INFINITY,
|
|||
|
|
f32::NEG_INFINITY,
|
|||
|
|
f32::INFINITY,
|
|||
|
|
f32::NEG_INFINITY,
|
|||
|
|
),
|
|||
|
|
|(mnx, mxx, mny, mxy), &[x, y, _]| {
|
|||
|
|
(mnx.min(x), mxx.max(x), mny.min(y), mxy.max(y))
|
|||
|
|
},
|
|||
|
|
);
|
|||
|
|
let aabb_hits =
|
|||
|
|
max_x >= vp_x0 && min_x <= vp_x1 && max_y >= vp_y0 && min_y <= vp_y1;
|
|||
|
|
if !any_near && !aabb_hits {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let clipped =
|
|||
|
|
clip_polyline_to_rect(&projected_pts, vp_x0, vp_y0, vp_x1, vp_y1, pcz);
|
|||
|
|
if clipped.is_empty() {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Paper-space AABB of the clipped polyline — the cloned model
|
|||
|
|
// (UTM) AABB would make click_hit's screen-projected pre-reject
|
|||
|
|
// discard the wire (box selection has no pre-reject, which is why
|
|||
|
|
// it kept working while picking didn't).
|
|||
|
|
let mut pmnx = f32::INFINITY;
|
|||
|
|
let mut pmny = f32::INFINITY;
|
|||
|
|
let mut pmxx = f32::NEG_INFINITY;
|
|||
|
|
let mut pmxy = f32::NEG_INFINITY;
|
|||
|
|
for &[x, y, _] in clipped.iter().filter(|p| p[0].is_finite()) {
|
|||
|
|
pmnx = pmnx.min(x);
|
|||
|
|
pmny = pmny.min(y);
|
|||
|
|
pmxx = pmxx.max(x);
|
|||
|
|
pmxy = pmxy.max(y);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Project snap points + key vertices into the same paper frame,
|
|||
|
|
// keeping only those inside the viewport rect, so endpoint /
|
|||
|
|
// midpoint / centre snaps land on the visible sheet geometry
|
|||
|
|
// instead of the model's UTM coordinates.
|
|||
|
|
let snap_pts: Vec<(glam::DVec3, model::wire_model::SnapHint)> = wire
|
|||
|
|
.snap_pts
|
|||
|
|
.iter()
|
|||
|
|
.filter_map(|(w, h)| {
|
|||
|
|
let p = proj_abs(w.x, w.y, w.z);
|
|||
|
|
(p[0].is_finite() && in_vp(p[0], p[1]))
|
|||
|
|
.then(|| (glam::DVec3::new(p[0] as f64, p[1] as f64, p[2] as f64), *h))
|
|||
|
|
})
|
|||
|
|
.collect();
|
|||
|
|
let key_vertices: Vec<[f64; 3]> = wire
|
|||
|
|
.key_vertices
|
|||
|
|
.iter()
|
|||
|
|
.filter_map(|&[kx, ky, kz]| {
|
|||
|
|
let p = proj_abs(kx, ky, kz);
|
|||
|
|
(p[0].is_finite() && in_vp(p[0], p[1]))
|
|||
|
|
.then(|| [p[0] as f64, p[1] as f64, p[2] as f64])
|
|||
|
|
})
|
|||
|
|
.collect();
|
|||
|
|
|
|||
|
|
let adapted = view::render::adapt_to_bg(wire.color, self.paper_bg_color);
|
|||
|
|
let [r, g, b, a] = adapted;
|
|||
|
|
let mut out = wire.clone();
|
|||
|
|
out.points = clipped;
|
|||
|
|
// Paper coordinates are small sheet units — no relative-to-eye
|
|||
|
|
// residual is needed, and keeping the model wire's points_low
|
|||
|
|
// here would add a model-scale offset to the paper points.
|
|||
|
|
out.points_low = Vec::new();
|
|||
|
|
out.snap_pts = snap_pts;
|
|||
|
|
out.key_vertices = key_vertices;
|
|||
|
|
// Tangent geometry is in model space and can't be trivially
|
|||
|
|
// re-expressed in paper coords — drop it (no tangent snap on
|
|||
|
|
// projected viewport content) rather than snap to UTM.
|
|||
|
|
out.tangent_geoms = Vec::new();
|
|||
|
|
out.aabb = if pmnx.is_finite() {
|
|||
|
|
[pmnx, pmny, pmxx, pmxy]
|
|||
|
|
} else {
|
|||
|
|
WireModel::UNBOUNDED_AABB
|
|||
|
|
};
|
|||
|
|
out.color = [r * 0.80, g * 0.80, b * 0.80, a * 0.85];
|
|||
|
|
out.line_weight_px = wire.line_weight_px;
|
|||
|
|
// Wire's pattern was sized for model-space coords during
|
|||
|
|
// tessellation; we just projected points into paper coords
|
|||
|
|
// (× scale), so rescale the dash pattern by the same factor
|
|||
|
|
// to keep dimensional consistency in the GPU shader.
|
|||
|
|
out.pattern_length = wire.pattern_length * scale;
|
|||
|
|
out.pattern = wire.pattern.map(|v| v * scale);
|
|||
|
|
out.vp_scissor = Some([vp_x0, vp_y0, vp_x1, vp_y1]);
|
|||
|
|
projected.push(out);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Store in cache, then extend result.
|
|||
|
|
self.paper_projected_cache
|
|||
|
|
.borrow_mut()
|
|||
|
|
.insert(vp_handle, (self.geometry_epoch, projected.clone()));
|
|||
|
|
result.extend(projected);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
result
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Paper boundary wire ────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
// ── Cohen-Sutherland line clipping ───────────────────────────────────────
|
|||
|
|
|
|||
|
|
/// Clip a single segment (x0,y0)→(x1,y1) against the axis-aligned rectangle
|
|||
|
|
/// [xmin,xmax]×[ymin,ymax]. Returns the clipped endpoints or `None` if the
|
|||
|
|
/// segment is entirely outside.
|
|||
|
|
|
|||
|
|
fn cs_clip(
|
|||
|
|
mut x0: f32,
|
|||
|
|
mut y0: f32,
|
|||
|
|
mut x1: f32,
|
|||
|
|
mut y1: f32,
|
|||
|
|
xmin: f32,
|
|||
|
|
ymin: f32,
|
|||
|
|
xmax: f32,
|
|||
|
|
ymax: f32,
|
|||
|
|
) -> Option<(f32, f32, f32, f32)> {
|
|||
|
|
const LEFT: u8 = 1;
|
|||
|
|
const RIGHT: u8 = 2;
|
|||
|
|
const BOTTOM: u8 = 4;
|
|||
|
|
const TOP: u8 = 8;
|
|||
|
|
|
|||
|
|
let code = |x: f32, y: f32| -> u8 {
|
|||
|
|
let mut c = 0u8;
|
|||
|
|
if x < xmin {
|
|||
|
|
c |= LEFT;
|
|||
|
|
} else if x > xmax {
|
|||
|
|
c |= RIGHT;
|
|||
|
|
}
|
|||
|
|
if y < ymin {
|
|||
|
|
c |= BOTTOM;
|
|||
|
|
} else if y > ymax {
|
|||
|
|
c |= TOP;
|
|||
|
|
}
|
|||
|
|
c
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let mut c0 = code(x0, y0);
|
|||
|
|
let mut c1 = code(x1, y1);
|
|||
|
|
|
|||
|
|
loop {
|
|||
|
|
if c0 | c1 == 0 {
|
|||
|
|
return Some((x0, y0, x1, y1));
|
|||
|
|
}
|
|||
|
|
if c0 & c1 != 0 {
|
|||
|
|
return None;
|
|||
|
|
}
|
|||
|
|
let cout = if c0 != 0 { c0 } else { c1 };
|
|||
|
|
let (x, y);
|
|||
|
|
if cout & TOP != 0 {
|
|||
|
|
x = x0 + (x1 - x0) * (ymax - y0) / (y1 - y0);
|
|||
|
|
y = ymax;
|
|||
|
|
} else if cout & BOTTOM != 0 {
|
|||
|
|
x = x0 + (x1 - x0) * (ymin - y0) / (y1 - y0);
|
|||
|
|
y = ymin;
|
|||
|
|
} else if cout & RIGHT != 0 {
|
|||
|
|
y = y0 + (y1 - y0) * (xmax - x0) / (x1 - x0);
|
|||
|
|
x = xmax;
|
|||
|
|
} else {
|
|||
|
|
y = y0 + (y1 - y0) * (xmin - x0) / (x1 - x0);
|
|||
|
|
x = xmin;
|
|||
|
|
}
|
|||
|
|
if cout == c0 {
|
|||
|
|
x0 = x;
|
|||
|
|
y0 = y;
|
|||
|
|
c0 = code(x0, y0);
|
|||
|
|
} else {
|
|||
|
|
x1 = x;
|
|||
|
|
y1 = y;
|
|||
|
|
c1 = code(x1, y1);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Clip a projected polyline (NaN-separated segments) to the viewport rectangle.
|
|||
|
|
/// Returns a new points vec with proper NaN separators at clip boundaries.
|
|||
|
|
fn clip_polyline_to_rect(
|
|||
|
|
pts: &[[f32; 3]],
|
|||
|
|
xmin: f32,
|
|||
|
|
ymin: f32,
|
|||
|
|
xmax: f32,
|
|||
|
|
ymax: f32,
|
|||
|
|
z: f32,
|
|||
|
|
) -> Vec<[f32; 3]> {
|
|||
|
|
const NAN3: [f32; 3] = [f32::NAN, f32::NAN, f32::NAN];
|
|||
|
|
let mut result: Vec<[f32; 3]> = Vec::new();
|
|||
|
|
let mut i = 0;
|
|||
|
|
|
|||
|
|
while i < pts.len() {
|
|||
|
|
// Skip NaN separators.
|
|||
|
|
if pts[i][0].is_nan() || pts[i][1].is_nan() {
|
|||
|
|
i += 1;
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
// Gather contiguous run of finite points.
|
|||
|
|
let start = i;
|
|||
|
|
while i < pts.len() && pts[i][0].is_finite() && pts[i][1].is_finite() {
|
|||
|
|
i += 1;
|
|||
|
|
}
|
|||
|
|
let seg = &pts[start..i];
|
|||
|
|
if seg.len() < 2 {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Clip each edge and track pen state to insert NaN on lift.
|
|||
|
|
let mut pen_down = false;
|
|||
|
|
for j in 0..seg.len() - 1 {
|
|||
|
|
let [x0, y0, _] = seg[j];
|
|||
|
|
let [x1, y1, _] = seg[j + 1];
|
|||
|
|
match cs_clip(x0, y0, x1, y1, xmin, ymin, xmax, ymax) {
|
|||
|
|
None => {
|
|||
|
|
pen_down = false;
|
|||
|
|
}
|
|||
|
|
Some((cx0, cy0, cx1, cy1)) => {
|
|||
|
|
if !pen_down {
|
|||
|
|
if !result.is_empty() {
|
|||
|
|
result.push(NAN3);
|
|||
|
|
}
|
|||
|
|
result.push([cx0, cy0, z]);
|
|||
|
|
pen_down = true;
|
|||
|
|
} else if let Some(&[lx, ly, _]) = result.last() {
|
|||
|
|
if (lx - cx0).abs() > 1e-4 || (ly - cy0).abs() > 1e-4 {
|
|||
|
|
result.push(NAN3);
|
|||
|
|
result.push([cx0, cy0, z]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
result.push([cx1, cy1, z]);
|
|||
|
|
// If the exit point was clipped, lift pen.
|
|||
|
|
if (cx1 - x1).abs() > 1e-4 || (cy1 - y1).abs() > 1e-4 {
|
|||
|
|
pen_down = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// Remove trailing NaN.
|
|||
|
|
while result
|
|||
|
|
.last()
|
|||
|
|
.map(|p: &[f32; 3]| p[0].is_nan())
|
|||
|
|
.unwrap_or(false)
|
|||
|
|
{
|
|||
|
|
result.pop();
|
|||
|
|
}
|
|||
|
|
result
|
|||
|
|
}
|