| … | … | @@ -45,6 +45,20 @@ and `gamma` echoes its arguments back as `B= G= R=`. |
| 45 | 45 | Empty slots pad `0 0 0`. With `INVERT_HIGH_AND_LOW` in the firmware, input 0 is |
| 46 | 46 | LED-off, so padding is dark. |
| 47 | 47 | |
| 48 | ## Intensity, and what does not work yet | |
| 49 | ||
| 50 | Only **colour** reaches the hardware. `Scale by Energy` is off by default and is | |
| 51 | marked provisional, because it is not physically motivated as it stands: Blender | |
| 52 | Power is watts against an arbitrary reference, while a diode's luminous output | |
| 53 | is fixed by the part. PWM duty sets perceived brightness; it cannot raise the | |
| 54 | ceiling. So normalising Power against `Energy Reference` maps to nothing real. | |
| 55 | ||
| 56 | **Future work:** identify the exact diode type per channel and derive a measured | |
| 57 | per-channel PWM ceiling from its spec — the same shape as Red Max PWM below, but | |
| 58 | grounded in the part rather than picked by hand. That is the correct way to say | |
| 59 | "how bright can this channel actually get", and it should replace the Scale by | |
| 60 | Energy switch rather than sit beside it. | |
| 61 | ||
| 48 | 62 | ## Red Max PWM |
| 49 | 63 | |
| 50 | 64 | Per-fixture setting (the fleet is mixed, so a single global value would be |
| … | … | @@ -101,8 +115,19 @@ channelbag and only the slot bound to that datablock may be read. |
| 101 | 115 | |
| 102 | 116 | ## Sending |
| 103 | 117 | |
| 104 | `LIVE` arms output. Frames are sent on frame change and on viewport edit | |
| 105 | (both toggleable under Output). | |
| 118 | **Arm `LIVE` and output is automatic.** There is no "send" step in normal use -- | |
| 119 | playback, scrubbing/seeking anywhere in the timeline, and any light colour or | |
| 120 | energy edit all push on their own (both sources toggleable under Output). | |
| 121 | ||
| 122 | The Tools menu (▾ beside Add/Remove) holds the two things that are *not* part of | |
| 123 | that loop: | |
| 124 | ||
| 125 | - **Convert Name-Encoded Lights** — one-time migration for older .blend files | |
| 126 | that stored wiring in object names. Does nothing in a scene built with | |
| 127 | Add Fixture. | |
| 128 | - **Resend / Force Refresh** — recovery only: push once with `LIVE` off, or | |
| 129 | re-send after a fixture reconnects (an identical payload is otherwise | |
| 130 | suppressed, so a returning fixture can sit dark until the next real change). | |
| 106 | 131 | |
| 107 | 132 | - **Coalescing** — one pending frame per fixture; if the UI outruns the wire, |
| 108 | 133 | intermediate frames are dropped rather than queued, so output never plays back |
| … | … | @@ -112,8 +112,12 @@ class CTHULOID_OT_sync_names(Operator): |
| 112 | 112 | |
| 113 | 113 | class CTHULOID_OT_adopt_scene(Operator): |
| 114 | 114 | bl_idname = "cthuloid.adopt_scene" |
| 115 | bl_label = "Adopt Existing Lights" | |
| 116 | bl_description = "Import legacy bgr-<slot>-<host> named lights into fixtures" | |
| 115 | bl_label = "Convert Name-Encoded Lights" | |
| 116 | bl_description = ( | |
| 117 | "One-time migration for older .blend files. Older rigs stored the " | |
| 118 | "wiring in the object name (bgr-1-192.168.50.138 meant slot 1 on that " | |
| 119 | "host); this converts them to fixtures. Does nothing in a scene built " | |
| 120 | "with Add Fixture") | |
| 117 | 121 | bl_options = {'REGISTER', 'UNDO'} |
| 118 | 122 | |
| 119 | 123 | def execute(self, context): |
| … | … | @@ -156,8 +160,12 @@ class CTHULOID_OT_assign_light(Operator): |
| 156 | 160 | |
| 157 | 161 | class CTHULOID_OT_send_now(Operator): |
| 158 | 162 | bl_idname = "cthuloid.send_now" |
| 159 | bl_label = "Send Now" | |
| 160 | bl_description = "Push the current scene state to all enabled fixtures" | |
| 163 | bl_label = "Resend / Force Refresh" | |
| 164 | bl_description = ( | |
| 165 | "Recovery only -- output is normally automatic while LIVE is armed. " | |
| 166 | "Use this to push once with LIVE off, or to re-send after a fixture " | |
| 167 | "reconnects, since an identical payload is otherwise suppressed and a " | |
| 168 | "returning fixture can sit dark until the next real change") | |
| 161 | 169 | |
| 162 | 170 | def execute(self, context): |
| 163 | 171 | n = engine.push_scene(context.scene, force=True) |
| … | … | @@ -62,12 +62,26 @@ class CthuloidFixture(PropertyGroup): |
| 62 | 62 | "rig shipped with; the firmware itself hard-rejects anything above " |
| 63 | 63 | "45000 and will discard the entire command, so that is the maximum")) |
| 64 | 64 | |
| 65 | # PROVISIONAL -- off by default, and physically unmotivated as it stands. | |
| 66 | # Blender Power is watts against an arbitrary reference, whereas a diode's | |
| 67 | # luminous output is fixed by the part: PWM duty sets perceived brightness, | |
| 68 | # it cannot raise the ceiling. Normalising Power against energy_ref | |
| 69 | # therefore maps nothing real. | |
| 70 | # | |
| 71 | # TODO: identify the exact diode type per channel and derive a measured | |
| 72 | # per-channel PWM ceiling from its spec -- the same shape as red_max_pwm, | |
| 73 | # but grounded in the part rather than picked by hand. That is the correct | |
| 74 | # way to express "how bright can this channel actually get", and it should | |
| 75 | # replace this switch rather than sit beside it. | |
| 65 | 76 | use_energy: BoolProperty( |
| 66 | name="Scale by Energy", default=False, | |
| 77 | name="Scale by Energy (provisional)", default=False, | |
| 67 | 78 | description=( |
| 68 | 79 | "Multiply each light's colour by its Power, normalised against " |
| 69 | "Energy Reference. Off matches the original script, which read " | |
| 70 | "colour only and ignored Power")) | |
| 80 | "Energy Reference. Provisional and off by default: a diode's output " | |
| 81 | "ceiling is fixed by the part, so Blender Power does not map to " | |
| 82 | "anything physical. Pending per-channel ceilings derived from the " | |
| 83 | "actual diode spec. Off matches the original script, which read " | |
| 84 | "colour only")) | |
| 71 | 85 | |
| 72 | 86 | energy_ref: FloatProperty( |
| 73 | 87 | name="Energy Reference", default=1000.0, min=0.001, |
| … | … | @@ -25,6 +25,19 @@ class CTHULOID_UL_fixtures(UIList): |
| 25 | 25 | row.label(text="", icon=icons.get(status, 'RADIOBUT_OFF')) |
| 26 | 26 | |
| 27 | 27 | |
| 28 | class CTHULOID_MT_tools(bpy.types.Menu): | |
| 29 | """Occasional operations. Everything here is either a one-time migration or | |
| 30 | a recovery action -- normal output is automatic while LIVE is armed.""" | |
| 31 | bl_label = "Tools" | |
| 32 | bl_idname = "CTHULOID_MT_tools" | |
| 33 | ||
| 34 | def draw(self, context): | |
| 35 | layout = self.layout | |
| 36 | layout.operator("cthuloid.send_now", icon='FILE_REFRESH') | |
| 37 | layout.separator() | |
| 38 | layout.operator("cthuloid.adopt_scene", icon='IMPORT') | |
| 39 | ||
| 40 | ||
| 28 | 41 | class CthuloidPanelBase: |
| 29 | 42 | bl_space_type = 'VIEW_3D' |
| 30 | 43 | bl_region_type = 'UI' |
| … | … | @@ -50,10 +63,10 @@ class CTHULOID_PT_main(CthuloidPanelBase, Panel): |
| 50 | 63 | col.operator("cthuloid.add_fixture", text="", icon='ADD') |
| 51 | 64 | col.operator("cthuloid.remove_fixture", text="", icon='REMOVE') |
| 52 | 65 | col.separator() |
| 53 | col.operator("cthuloid.adopt_scene", text="", icon='IMPORT') | |
| 66 | col.menu("CTHULOID_MT_tools", text="", icon='DOWNARROW_HLT') | |
| 54 | 67 | |
| 55 | row = layout.row(align=True) | |
| 56 | row.operator("cthuloid.send_now", icon='EXPORT') | |
| 68 | if props.live: | |
| 69 | layout.label(text="Sending on playback, seek and edits", icon='CHECKMARK') | |
| 57 | 70 | |
| 58 | 71 | |
| 59 | 72 | class CTHULOID_PT_fixture(CthuloidPanelBase, Panel): |
| … | … | @@ -206,6 +219,7 @@ class CTHULOID_PT_light(CthuloidPanelBase, Panel): |
| 206 | 219 | |
| 207 | 220 | CLASSES = ( |
| 208 | 221 | CTHULOID_UL_fixtures, |
| 222 | CTHULOID_MT_tools, | |
| 209 | 223 | CTHULOID_PT_main, |
| 210 | 224 | CTHULOID_PT_fixture, |
| 211 | 225 | CTHULOID_PT_hierarchy, |