forked from 1b-agent/opencadstudio-upstream
Map Light; record SectionSymbol as blocked
Light placement, intensity, colour, attenuation and shadow fields are writable with host validation. staged_python_light_lifecycle_over_real_ipc passes every gate except DXF cast_shadows, pinned by a canary. SectionSymbol is recorded as blocked: it needs host-created style and view representation objects and its raw point counts have no documented derivation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
21ce4b0e2b
commit
12b0823e76
10 files changed
+158
-12
No files matched your search
@@ -796,6 +796,7 @@ fn generate_entity_coverage(out_dir: &Path, registry: &TypeRegistry) {
|
||||
| ("Underlay", _)
|
||||
| ("Viewport", _)
|
||||
| ("ViewBorder", _)
|
||||
| ("Light", _)
|
||||
| (
|
||||
"MLine",
|
||||
"flags"
|
||||
|
||||
@@ -462,6 +462,29 @@
|
||||
"rotation_angle",
|
||||
"active_viewport",
|
||||
"scale_handle"
|
||||
],
|
||||
"Light": [
|
||||
"layer",
|
||||
"name",
|
||||
"light_type",
|
||||
"position",
|
||||
"target",
|
||||
"status",
|
||||
"light_color",
|
||||
"plot_glyph",
|
||||
"intensity",
|
||||
"attenuation_type",
|
||||
"use_attenuation_limits",
|
||||
"attenuation_start_limit",
|
||||
"attenuation_end_limit",
|
||||
"hotspot_angle",
|
||||
"falloff_angle",
|
||||
"cast_shadows",
|
||||
"shadow_type",
|
||||
"shadow_map_size",
|
||||
"shadow_map_softness",
|
||||
"photometric_mode",
|
||||
"photometric_data"
|
||||
]
|
||||
},
|
||||
"readable": {
|
||||
@@ -566,6 +589,9 @@
|
||||
],
|
||||
"ViewBorder": [
|
||||
"version"
|
||||
],
|
||||
"Light": [
|
||||
"class_version"
|
||||
]
|
||||
},
|
||||
"aliases": {
|
||||
|
||||
@@ -975,6 +975,51 @@ fn validate_view_border(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Light: type, placement, intensity, attenuation and shadow settings.
|
||||
#[cfg(feature = "host")]
|
||||
fn validate_light(
|
||||
old: Option<&crate::host::acadrust::entities::Light>,
|
||||
new: &crate::host::acadrust::entities::Light,
|
||||
) -> Result<(), String> {
|
||||
if old == Some(new) {
|
||||
return Ok(());
|
||||
}
|
||||
if new.name.trim().is_empty() {
|
||||
return Err("Light.name is empty".into());
|
||||
}
|
||||
if !(1..=3).contains(&new.light_type) {
|
||||
return Err("Light.light_type must be 1 (distant), 2 (point) or 3 (spot)".into());
|
||||
}
|
||||
finite_vector("Light.position", &new.position)?;
|
||||
finite_vector("Light.target", &new.target)?;
|
||||
if new.light_type != 2 && new.position == new.target {
|
||||
return Err("a distant or spot Light needs a target different from its position".into());
|
||||
}
|
||||
if !new.intensity.is_finite() || new.intensity < 0.0 {
|
||||
return Err("Light.intensity must be finite and non-negative".into());
|
||||
}
|
||||
for (name, value) in [
|
||||
("attenuation_start_limit", new.attenuation_start_limit),
|
||||
("attenuation_end_limit", new.attenuation_end_limit),
|
||||
("hotspot_angle", new.hotspot_angle),
|
||||
("falloff_angle", new.falloff_angle),
|
||||
] {
|
||||
if !value.is_finite() || value < 0.0 {
|
||||
return Err(format!("Light.{name} must be finite and non-negative"));
|
||||
}
|
||||
}
|
||||
if new.use_attenuation_limits && new.attenuation_start_limit > new.attenuation_end_limit {
|
||||
return Err("Light attenuation start limit must not exceed the end limit".into());
|
||||
}
|
||||
if new.light_type == 3 && new.hotspot_angle > new.falloff_angle {
|
||||
return Err("Light.hotspot_angle must not exceed falloff_angle".into());
|
||||
}
|
||||
if new.shadow_map_size < 0 {
|
||||
return Err("Light.shadow_map_size must be non-negative".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate newly created geometry for the kinds whose mapped fields have
|
||||
/// host checks. Called by the Python add path before an entity enters the document.
|
||||
#[cfg(feature = "host")]
|
||||
@@ -1080,6 +1125,7 @@ pub fn validate_new_canvas_entity(entity: &crate::host::EntityType) -> Result<()
|
||||
EntityType::Underlay(value) => validate_underlay(None, value)?,
|
||||
EntityType::Viewport(value) => validate_viewport(None, value)?,
|
||||
EntityType::ViewBorder(value) => validate_view_border(None, value)?,
|
||||
EntityType::Light(value) => validate_light(None, value)?,
|
||||
EntityType::AttributeDefinition(value) => {
|
||||
finite_vector(
|
||||
"AttributeDefinition.insertion_point",
|
||||
@@ -1446,6 +1492,7 @@ pub fn validate_entity_mutation(
|
||||
(EntityType::Underlay(old), EntityType::Underlay(new)) => validate_underlay(Some(old), new)?,
|
||||
(EntityType::Viewport(old), EntityType::Viewport(new)) => validate_viewport(Some(old), new)?,
|
||||
(EntityType::ViewBorder(old), EntityType::ViewBorder(new)) => validate_view_border(Some(old), new)?,
|
||||
(EntityType::Light(old), EntityType::Light(new)) => validate_light(Some(old), new)?,
|
||||
(EntityType::MLine(old), EntityType::MLine(new)) => validate_mline(Some(old), new)?,
|
||||
(EntityType::Leader(old), EntityType::Leader(new)) => validate_leader(Some(old), new)?,
|
||||
(EntityType::AttributeDefinition(old), EntityType::AttributeDefinition(new)) => {
|
||||
@@ -2184,6 +2231,7 @@ mod tests {
|
||||
| "Underlay"
|
||||
| "Viewport"
|
||||
| "ViewBorder"
|
||||
| "Light"
|
||||
)
|
||||
{
|
||||
assert!(
|
||||
@@ -2506,6 +2554,26 @@ mod tests {
|
||||
"ViewBorder.rotation_angle".to_owned(),
|
||||
"ViewBorder.active_viewport".to_owned(),
|
||||
"ViewBorder.scale_handle".to_owned(),
|
||||
"Light.name".to_owned(),
|
||||
"Light.light_type".to_owned(),
|
||||
"Light.position".to_owned(),
|
||||
"Light.target".to_owned(),
|
||||
"Light.status".to_owned(),
|
||||
"Light.light_color".to_owned(),
|
||||
"Light.plot_glyph".to_owned(),
|
||||
"Light.intensity".to_owned(),
|
||||
"Light.attenuation_type".to_owned(),
|
||||
"Light.use_attenuation_limits".to_owned(),
|
||||
"Light.attenuation_start_limit".to_owned(),
|
||||
"Light.attenuation_end_limit".to_owned(),
|
||||
"Light.hotspot_angle".to_owned(),
|
||||
"Light.falloff_angle".to_owned(),
|
||||
"Light.cast_shadows".to_owned(),
|
||||
"Light.shadow_type".to_owned(),
|
||||
"Light.shadow_map_size".to_owned(),
|
||||
"Light.shadow_map_softness".to_owned(),
|
||||
"Light.photometric_mode".to_owned(),
|
||||
"Light.photometric_data".to_owned(),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,13 +7,13 @@ Build a general Python document model over the OCS host API for all **43 canvas
|
||||
As of 20 September 2026, the working branch is `plugin/host-model-api` at
|
||||
`a8dd3640` plus the Leader increment, published as draft PR
|
||||
[#1391](https://github.com/HakanSeven12/OpenCADStudio/pull/1391). Host API is
|
||||
v7. The Python feature has explicit writes for **36 of 43** canvas kinds: 35
|
||||
are creatable and `Insert` remains update-only. This is **not** 36 completed
|
||||
v7. The Python feature has explicit writes for **37 of 43** canvas kinds: 36
|
||||
are creatable and `Insert` remains update-only. This is **not** 37 completed
|
||||
kinds: Tolerance, Shape, AttributeEntity, Hatch, MLine, Dimension, MultiLeader, Table, PolygonMesh, PolyfaceMesh, Mesh and Wipeout pass every completion gate.
|
||||
AttributeDefinition passes its host and real IPC lifecycle but remains short of
|
||||
`Complete` because the previously pinned CAD codec dropped optional ATTDEF
|
||||
fields during DXF reads; revalidate that blocker against the current
|
||||
`acadrust` revision `7ea4247`. Leader is mapped and integration-tested but blocked at W by three acadrust persistence gaps (see the ledger). Leader, Helix, Underlay and ViewBorder are mapped and integration-tested but blocked at W by acadrust persistence gaps, and RasterImage and Viewport are held short of `Complete` by unverifiable or missing oracles (see the ledger). RasterImage is mapped and integration-tested, held short of `Complete` by unverifiable image-dictionary linkage. The other 7 canvas kinds have only `layer`
|
||||
`acadrust` revision `7ea4247`. Leader is mapped and integration-tested but blocked at W by three acadrust persistence gaps (see the ledger). Leader, Helix, Underlay, ViewBorder and Light are mapped and integration-tested but blocked at W by acadrust persistence gaps, and RasterImage and Viewport are held short of `Complete` by unverifiable or missing oracles (see the ledger). RasterImage is mapped and integration-tested, held short of `Complete` by unverifiable image-dictionary linkage. The other 6 canvas kinds have only `layer` (SectionSymbol is recorded as blocked)
|
||||
writes through the document model. All 43 have raw typed snapshots. There are
|
||||
three internal records and two opaque fallbacks outside the 43 canvas kinds.
|
||||
|
||||
@@ -123,7 +123,7 @@ The last five may require new engine capabilities rather than only Python conver
|
||||
|
||||
## Handoff start point
|
||||
|
||||
Continue with **SectionSymbol** (queue item 15). Leader is done except for its W
|
||||
Continue with **Region** (queue item 17). SectionSymbol (item 15) is blocked; see the ledger. Leader is done except for its W
|
||||
gate; when the engine writes DXF group 340, reads group 213 and the DWG
|
||||
R2010+ text-size question is decided, flip the canary assertions in
|
||||
`staged_python_leader_lifecycle_over_real_ipc` and mark it `Complete`.
|
||||
@@ -169,3 +169,4 @@ exact queue above and update statuses with evidence after each increment.
|
||||
- Mapped `Underlay` against a fixture PDF `UnderlayDefinition`. All gates pass except DXF W: the reader does not convert the degree-valued rotation back to radians, and repeated DXF round trips compound the error (pinned by a canary). The queue now continues with `Viewport`.
|
||||
- Mapped `Viewport` with a paper-space owner rule and frozen-layer reference validation. Every gate except a paper-space canvas oracle passes through the shared driver; the kind stays integration-tested. The queue now continues with `ViewBorder`.
|
||||
- Mapped `ViewBorder` with viewport and scale reference rules. All gates pass in DWG; DXF loading does not restore the typed entity (pinned by a canary). The queue now continues with `SectionSymbol`.
|
||||
- Recorded `SectionSymbol` (queue item 15) as **blocked**: it needs a section view style and view representation the host cannot create, and its raw point-count fields have no documented derivation. Mapped `Light` (item 16); every gate passes except DXF `cast_shadows`, which the reader drops (pinned by a canary). The queue now continues with `Region`.
|
||||
@@ -1,6 +1,6 @@
|
||||
# OCS canvas kind coverage ledger
|
||||
|
||||
Use with [the completion plan](plugin-host-model-completion-plan.md). This ledger is current as of 20 September 2026: **36 of 43 canvas kinds have explicit geometry writes** (35 creatable, plus update-only `Insert`), **12 kinds are complete**, and 7 remain layer-only. `Mapped` means at least one non-layer field is exposed for Python writes; it does **not** mean the kind passes the completion gate. `Update-only` means Python creation is unavailable. `Layer-only` means raw inspection and canvas layer edits exist, but geometry is not mapped. Record actual evidence or a blocking issue before changing any status to `Complete`.
|
||||
Use with [the completion plan](plugin-host-model-completion-plan.md). This ledger is current as of 20 September 2026: **37 of 43 canvas kinds have explicit geometry writes** (36 creatable, plus update-only `Insert`), **12 kinds are complete**, and 6 remain layer-only (one of them, SectionSymbol, blocked). `Mapped` means at least one non-layer field is exposed for Python writes; it does **not** mean the kind passes the completion gate. `Update-only` means Python creation is unavailable. `Layer-only` means raw inspection and canvas layer edits exist, but geometry is not mapped. Record actual evidence or a blocking issue before changing any status to `Complete`.
|
||||
|
||||
Completion gate codes: **C** = create through Python document model; **R** = read; **E** = edit defining geometry and another property; **D** = delete; **U** = undo/redo and atomic failure; **I** = real IPC plus GUI/canvas pick or observable behavior; **W** = DWG and DXF save/reopen; **V** = invalid-input checks and untouched-field/reference preservation; **P** = build and load the in-tree bundled plugin with a manifest matching the host toolchain and acadrust source. A kind is `Complete` only when **C/R/E/D/U/I/W/V/P** all pass and its property catalog matches implementation. `Blocked` requires a specific dependency or failing fixture.
|
||||
|
||||
@@ -42,8 +42,8 @@ Completion gate codes: **C** = create through Python document model; **R** = rea
|
||||
| Underlay | **Integration-tested / blocked at W by acadrust (DXF rotation units)** | Writable: `underlay_type` and `definition_handle` (both fixed by the definition: creation names an existing `UnderlayDefinition`, and a mismatched type or missing definition is rejected), `insertion_point`, `x_scale`, `y_scale`, `z_scale`, `rotation`, `normal`, `flags`, `contrast`, `fade`, `clip_boundary_vertices`, `clip_inverted`. The file path and page live on the definition object, which scripts cannot create or list yet; they reference one the drawing already owns. Host validation requires finite nonzero scales, a finite rotation, a nonzero normal, contrast/fade within 0..=100, known flags, finite clip vertices (none, or at least 2) and an existing definition of the same type. `staged_python_underlay_lifecycle_over_real_ipc` (shared `run_mesh_lifecycle` with a fixture PDF definition) passes C/R/E/D/U/I/V/P and DWG W: creation, a placement/scale/rotation/contrast/fade/clip transaction, canvas wires, eight rejection cases plus an atomic rollback, DWG reopen and re-edit, and three-step undo/redo. **W blocker (engine, pinned by canary assertions):** DXF stores the rotation in degrees but the reader returns it unconverted, so 0.5 rad reopens as 28.6 and each further DXF save-reopen compounds it (1641.4 after a second), silently corrupting the rotation. Not `Complete` until the reader converts degrees back to radians. Not covered: creating definitions from a file path, DWF/DGN fixtures. |
|
||||
| Viewport | **Integration-tested** | Writable: `center`, `width`, `height`, `status`, `view_center`, `view_direction`, `view_target`, `lens_length`, `front_clip_z`, `back_clip_z`, `view_height`, `twist_angle`, `custom_scale`, `frozen_layers` (layer handles), `render_mode`, `circle_sides`, `ucs_icon_visible`. Read-only: `id`, snap/grid settings, UCS fields, `clip_boundary_handle`, `style_sheet`, shade-plot, background, visual-style and sun handles, lighting, `ambient_color`, `elevation`. Reference rules: the owner must be a paper-space layout block (a model-space viewport is rejected) and every frozen-layer handle must be an existing layer. Host validation requires finite centers, positive width, height, view height, lens length and scale, a nonzero view direction, finite clip planes and twist, and at least 3 circle sides. `staged_python_viewport_lifecycle_over_real_ipc` (shared `run_mesh_lifecycle`, paper-space owner supplied by the driver) passes C/R/E/D/U/V/W/P: creation in the paper-space block, a move/resize/view-height/twist/frozen-layer transaction, seven rejection cases plus an atomic rollback, DWG and DXF reopen with re-edit, and three-step undo/redo. **Why not `Complete`:** the I gate lacks a canvas oracle because a paper-space viewport only draws on its layout tab, which the driver does not activate; viewport `id` uniqueness within a layout, clip boundary entities and view-to-scale synchronization are not handled by the host. |
|
||||
| ViewBorder | **Integration-tested / blocked at W by acadrust (DXF kind)** | Writable: `min`, `max`, `center` (2-D pairs), `scale`, `rotation_angle`, `active_viewport` and `scale_handle` (both required at creation). `version` is read-only. Reference rules: `active_viewport` must be an existing Viewport entity and `scale_handle` an existing `Scale` object. Host validation requires finite bounds with `min` below `max` on both axes, a positive scale and a finite rotation. `staged_python_view_border_lifecycle_over_real_ipc` (shared `run_mesh_lifecycle`, driver-made viewport and scale fixtures) passes C/R/E/D/U/I/V/P and DWG W: creation, a resize/recenter/scale/rotation transaction, canvas wires, seven rejection cases plus an atomic rollback, DWG reopen and re-edit, and three-step undo/redo. **W blocker (engine, pinned by a canary):** a DXF `DRAWINGVIEW` reopens as a different, opaque entity kind rather than as a ViewBorder, so only DWG restores the typed record. Not `Complete` until DXF loading restores the typed entity. Not covered: the linked view representation and drawing-view definition records. |
|
||||
| SectionSymbol | Layer-only | Requires section-view style and point records. |
|
||||
| Light | Layer-only | Requires position/target and photometric-state fixture. |
|
||||
| SectionSymbol | **Blocked** | Not mapped. A valid section mark needs a `SectionViewStyle` and a `ViewRep` view-representation object that a script cannot create through the host API, and the entity stores raw point counts and flag words (`raw_point_count_90`, `raw_flags_90`, `raw_point_record_count`, `raw_view_symbol_70`) whose relationship to `points` is undocumented in acadrust and is written back verbatim by both writers, so the host cannot keep them consistent after a scripted edit without guessing. Adding the kind needs (1) host-side creation or lookup of a section style and view representation, and (2) a confirmed derivation of the raw counts from a real DWG produced by another CAD application. The kind keeps raw snapshot access and layer edits. |
|
||||
| Light | **Integration-tested / blocked at W by acadrust (DXF `cast_shadows`)** | Writable: `name`, `light_type` (1 distant, 2 point, 3 spot), `position`, `target`, `status`, `light_color`, `plot_glyph`, `intensity`, the attenuation type and limits, `hotspot_angle`, `falloff_angle`, `cast_shadows`, `shadow_type`, `shadow_map_size`, `shadow_map_softness`, `photometric_mode`, `photometric_data`; `class_version` is read-only. Host validation requires a non-empty name, a known light type, finite placement, a spot or distant target that differs from the position, non-negative intensity, angles and limits, an attenuation start not above its end, a spot hotspot not above its falloff, and a non-negative shadow map size. `staged_python_light_lifecycle_over_real_ipc` (shared `run_mesh_lifecycle`) passes C/R/E/D/U/I/V/P and DWG W: a spot light with an RGB colour, an intensity/target/position/shadow transaction that preserves the colour and untouched fields, the canvas glyph, eight rejection cases plus an atomic rollback, DWG reopen and re-edit, and three-step undo/redo. **W blocker (engine, pinned by a canary):** the DXF reader does not restore `cast_shadows`, which reopens false; colour, placement and intensity persist. Not covered: photometric web data edits and sun/skylight objects. |
|
||||
| Region | Layer-only | Requires valid ACIS region payload. |
|
||||
| Body | Layer-only | Requires valid ACIS body payload. |
|
||||
| Solid3D | Layer-only | Requires valid ACIS solid payload. |
|
||||
|
||||
@@ -35,7 +35,7 @@ attached attribute and sequence records. Those fields report `transaction_geomet
|
||||
catalog. All 43 canvas kinds also support nonempty `layer` changes through
|
||||
the same undoable transaction; internal and opaque variants remain read-only
|
||||
through the document model. The adapter accepts only `layer` patches for
|
||||
canvas kinds outside its 36 geometry converters. Other writable properties
|
||||
canvas kinds outside its 37 geometry converters. Other writable properties
|
||||
still report `type_conversion_only`; broader CAD range and cross-property
|
||||
checks remain outstanding.
|
||||
The host's `entity_snapshot` helper serializes any typed entity as a detached
|
||||
@@ -91,7 +91,7 @@ Input tokens are bound to the drawing tab that requested the pick. Polling a
|
||||
token from another tab returns no result and does not consume it.
|
||||
|
||||
Python `doc.entities[handle]` returns a descriptor for every entity. For kinds
|
||||
outside the 36-kind generated schema it contains only handle, kind, and layer;
|
||||
outside the 37-kind generated schema it contains only handle, kind, and layer;
|
||||
`layer` is editable on canvas kinds. `doc.coverage()` enumerates the
|
||||
catalog; `doc.coverage("Line")` and `line.coverage` return one entry. For
|
||||
covered kinds, the coverage object lists actual readable and editable
|
||||
|
||||
@@ -94,7 +94,7 @@ The host-owned catalog classifies all 48 entity variants and tracks every
|
||||
traced field, including fields marked `unmapped`. Query it with
|
||||
`doc.coverage()` or `doc.coverage("Line")`; each row includes source field,
|
||||
type, access, and current validation status. Geometry edits remain limited to
|
||||
the 36 mapped kinds.
|
||||
the 37 mapped kinds.
|
||||
For any entity, `entity.snapshot` returns a detached dictionary of its raw
|
||||
CAD fields, including `snapshot["common"]` and kind-specific nested values.
|
||||
It can inspect an unmapped kind; editing the dictionary does
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
"Wipeout",
|
||||
"Underlay",
|
||||
"Viewport",
|
||||
"ViewBorder"
|
||||
"ViewBorder",
|
||||
"Light"
|
||||
],
|
||||
"manual_kinds": ["Dimension"],
|
||||
"base_fields": ["handle", "layer", "owner_handle"],
|
||||
@@ -413,6 +414,13 @@
|
||||
"active_viewport": { "required": true },
|
||||
"scale_handle": { "required": true }
|
||||
}
|
||||
},
|
||||
"Light": {
|
||||
"fields": {
|
||||
"class_version": { "read_only": true },
|
||||
"position": { "required": true },
|
||||
"name": { "required": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4756,7 +4756,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(emitted.len(), 36);
|
||||
assert_eq!(emitted.len(), 37);
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-host-model")]
|
||||
|
||||
@@ -3691,6 +3691,48 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn staged_python_light_lifecycle_over_real_ipc() {
|
||||
run_mesh_lifecycle(&MeshCase {
|
||||
create: concat!(
|
||||
"doc.create_entity('Light', name='Key', light_type=3, position=P(10.0, 10.0, 10.0),\n",
|
||||
" target=P(0.0, 0.0, 0.0), intensity=1.5, hotspot_angle=0.4, falloff_angle=0.8,\n",
|
||||
" light_color={'kind': 'Rgb', 'value': {'r': 255, 'g': 240, 'b': 200}})\n"),
|
||||
edit: concat!(
|
||||
"lt = doc.entities[HANDLE]\n",
|
||||
"with doc.transaction('Edit light'):\n",
|
||||
" lt.intensity = 3.0\n",
|
||||
" lt.target = (5.0, 5.0, 0.0)\n",
|
||||
" lt.position = (12.0, 8.0, 10.0)\n",
|
||||
" lt.cast_shadows = True\n",
|
||||
"doc.selection = [lt]\n"),
|
||||
rejects: &[
|
||||
("'light_type':9", "must be 1 (distant)"),
|
||||
("'intensity':-1.0", "non-negative"),
|
||||
("'target':{'x':12.0,'y':8.0,'z':10.0}", "different from its position"),
|
||||
("'hotspot_angle':2.0", "must not exceed falloff_angle"),
|
||||
("'position':{'x':float('nan'),'y':0.0,'z':0.0}", "finite"),
|
||||
("'name':''", "empty"),
|
||||
("'class_version':2", "read-only"),
|
||||
("'light_color':{'kind':'Index','value':300}", "Color"),
|
||||
],
|
||||
is_kind: |entity| matches!(entity, EntityType::Light(_)),
|
||||
digest: |entity| match entity {
|
||||
EntityType::Light(l) => format!("{} t{} i{:.1} pos{:.0},{:.0},{:.0} tgt{:.0},{:.0} sh{} {:?}", l.name, l.light_type,
|
||||
l.intensity, l.position.x, l.position.y, l.position.z, l.target.x, l.target.y, l.cast_shadows, l.light_color),
|
||||
_ => "wrong kind".into(),
|
||||
},
|
||||
reedit: |entity| if let EntityType::Light(l) = entity { l.intensity = 4.0; },
|
||||
expect_created: "Key t3 i1.5 pos10,10,10 tgt0,0 shfalse Rgb { r: 255, g: 240, b: 200 }",
|
||||
expect_edited: "Key t3 i3.0 pos12,8,10 tgt5,5 shtrue Rgb { r: 255, g: 240, b: 200 }",
|
||||
expect_reedited: "Key t3 i4.0 pos12,8,10 tgt5,5 shtrue Rgb { r: 255, g: 240, b: 200 }",
|
||||
// BLOCKER: DXF does not restore `cast_shadows` (it reopens false),
|
||||
// like the other boolean groups the reader parses as integers.
|
||||
expect_edited_dxf: "Key t3 i3.0 pos12,8,10 tgt5,5 shfalse Rgb { r: 255, g: 240, b: 200 }",
|
||||
expect_reedited_dxf: "Key t3 i4.0 pos12,8,10 tgt5,5 shfalse Rgb { r: 255, g: 240, b: 200 }",
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn staged_python_point_pick_and_cancel_over_real_ipc() {
|
||||
let Some(plugin_path) = std::env::var_os("OCS_TEST_PYTHON_PLUGIN") else {
|
||||
|
||||
Reference in new issue
Block a user