- Fields to Add to Your Light Snapper Class
- Fields to Remove
- Basic Interface Implementation
- Sync to LightGroup
- Field Replacements
- The Changed Event
- Clean out from Quick Props
- Light Number Fixes
- Conclusion
This article explains how to rewrite a light snapper to use LightGroups (new in 2.6).
(The SpotLightSnapper
class in cm/std/util3D will be used as example)
Fields to Add to Your Light Snapper Class
/** * Spotlight snapper (now rewritten for light groups). */ public class SpotLightSnapper extends AimingSnapper { /** * Light group key. */ private str _lightGroupKey; /** * Individual light settings. */ private LightGroup personal; /** * Light snapper number. */ private int _number : copy=null, ignore modify notice;
The _lightGroupKey
will identify the LightGroup
that this snapper belongs to. "Common" light groups are stored in space. If the snapper is set to have it's own individual settings, these will be stored in the 'personal' light group, but this group will not be stored in space.
The light snapper number is just a way of presenting the lights in the light group settings dialog, "Spot light 1" for instance.
Fields to Remove
Remove fields that we do not need any longer (will be replaced by light group props).
/** * Settings now stored in light group. */ //public double spotLightIntensity = defaultLightIntensity*4 : domain defaultLightIntensityRange; //public double effectRange = defaultEffectRange : domain SliderDoubleRange(0.5, 40); //public bool shadows = true : domain BoolSubSet(); //public double shadowSoftness = defaultShadowSoftness : domain SliderDoubleRange(0, 5); //public double shadowResolution = defaultShadowRes : domain SliderDoubleRange(0, 256); //public color spotColor = color(255, 255, 255); //public double temperature = 3500 : domain temperatureDomain; //public bool show3DOnPhoto = false : domain BoolSubSet();
Well, all properties that you think should be controllable per group should be removed. Fields that you want the user to control in quick props should be left as is. For the basic lights, I have left the on/off in the quick props (if you want to turn off the entire group, you can always lower the brightness to zero anyway).
/** * Individual light on/off. */ public bool on = true : domain BoolSubSet();
Basic Interface Implementation
/** * Individual light settings. Responsibility of light snappers. */ public LightGroup individualLightSettings() { if (!personal) { LightGroup g(null, "personal#" # id, individual=true); initOrRepair(g); personal = g; } assert(personal); return personal; } /** * Set individual light settings. Responsibility of light snappers. Return true if done. */ public void setIndividualLightSettings(LightGroup group) { this.personal = group; } /** * Return light group key, if any. Responsibility of light snappers. */ public str lightGroupKey() { //if (!_lightGroupKey and space) _lightGroupKey = space.anyLightGroupKey(); return _lightGroupKey; } /** * Set light group, if any. Responsibility of light snappers. */ public void setLightGroup(LightGroup group) { this._lightGroupKey = group ? group.key : null; if (showLightGroupIn2D) { invalidate2D(); graph = null; } }
Basic stuff, will look the same in all light snappers I think. You can ignore the showLightGroupIn2D
line for now.
Sync to LightGroup
When the snapper becomes a "member" of a LightGroup
, it will receive the syncToLightGroup()
event. Note that the snapper itself cannot create this light group, because it can be shared between many snappers, even of different classes. What the snapper can do, is to ensure that the group contains the properties it needs.
For instance, if the light group is only populated by spot light snappers, and your snapper has a special upLightIntensity
property, that has to be added to the group. This is done using LightGroup.repair
, see below ...
/** * Sync to to light group. */ public void syncToLightGroup() { if (LightGroup g = lightGroup()) { if (developMode) { bool verbose = true; str caller = this.class.name; g.typeCheck("intensity", Double, caller, verbose); g.typeCheck("effectRange", Double, caller, verbose); g.typeCheck("shadows", Bool, caller, verbose); g.typeCheck("shadowSoftness", Double, caller, verbose); g.typeCheck("shadowResolution", Double, caller, verbose); g.typeCheck("temperature", Double, caller, verbose); g.typeCheck("Show3DOnPhoto", Bool, caller, verbose); g.typeCheck("Show2DOnPrint", Bool, caller, verbose); g.typeCheck("spotConeAnge", Double, caller, verbose); } initOrRepair(g); // Re-apply cone angle (required because coneAngle is really a // field in the super class and not a pure light group property). coneAngle = angle(g.getDouble("spotConeAnge", coneAngle.deg*2)/2); } } /** * Init / repair light group. */ final private void initOrRepair(LightGroup g) { devassert(g) return; g.repair("intensity", $lightIntensity, lgDefaultLightIntensity, lgDefaultLightIntensityDomain); g.repair("effectRange", $lightEffectRange, lgDefaultEffectRange, lgDefaultERDomain); g.repair("quality", $lightQuality, lgDefaultLightQuality, lgDefaultQualityDomain); g.repair("shadows", $enableLightShadows, true, BoolSubSet()); g.repair("shadowSoftness", $shadowSoftness, lgDefaultShadowSoftness, lgDefaultSSDomain); g.repair("shadowResolution", $shadowResolution, lgDefaultShadowRes, lgDefaultSRDomain); g.repair("temperature", $lightTemperature, lgDefaultTemperature, lgTemperatureDomain); g.repair("Show3DOnPhoto", $lightShow3D, lgDefaultShowSpot3DOnPhoto, BoolSubSet()); g.repair("Show2DOnPrint", $show2DOnPrint, lgDefaultShow2DOnPrint, BoolSubSet()); g.repair("spotConeAnge", $lightConeWidth, lgDefaultSpotConeAngle, lgDefaultSCADomain); }
The type check is only for developers, but it should be there. If two snappers declare a property each with the same key but with different value types, we have a problem (and this will be detected by the type check). Naturally, if you want to share props with others, you must be compatible. (Even the valid subset (domain) should be identical.) And if you do not want to share, please make the property key unique enough ("MyCoolSnapper-upLightIntensity").
Field Replacements
/** * Props. */ final public double spotLightIntensity() { return lightGroup.getDouble("intensity", lgDefaultLightIntensity); } final public double spotLightIntensity=(double v) { lightGroup.setProp("intensity", Double(v)); return v; } final public double effectRange() { return lightGroup.getDouble("effectRange", lgDefaultEffectRange); } final public double effectRange=(double v) { lightGroup.setProp("effectRange", Double(v)); return v; } final public double lightQuality() { return lightGroup.getDouble("quality", lgDefaultLightQuality); } final public double lightQuality=(double v) { lightGroup.setProp("quality", Double(v)); return v; } final public bool shadows() { return lightGroup.getBool("shadows", default=true); } final public bool shadows=(bool v) { lightGroup.setProp("shadows", Bool(v)); return v; } final public double shadowSoftness() { return lightGroup.getDouble("shadowSoftness", lgDefaultShadowSoftness); } final public double shadowSoftness=(double v) { lightGroup.setProp("shadowSoftness", Double(v)); return v; } final public double shadowResolution() { return lightGroup.getDouble("shadowResolution", lgDefaultShadowRes); } final public double shadowResolution=(double v) { lightGroup.setProp("shadowResolution", Double(v)); return v; } final public color spotColor() { return fakeTemperatureToColor(temperature, aggressive=true); } //final public color spotColor() { return lightGroup.getColor("lightColor", color(255)); } //final public color spotColor=(color v) { lightGroup.setProp("lightColor", Color(v)); return v; } final public double temperature() { return lightGroup.getDouble("temperature", 3500); } final public double temperature=(double v) { lightGroup.setProp("temperature", Double(v)); return v; } final public bool show2DOnPrint() { return lightGroup.getBool("Show2DOnPrint", lgDefaultShow2DOnPrint); } final public bool show2DOnPrint=(bool v) { lightGroup.setProp("Show2DOnPrint", Bool(v)); return v; } final public bool show3DOnPhoto() { return lightGroup.getBool("Show3DOnPhoto", lgDefaultShowSpot3DOnPhoto); } final public bool show3DOnPhoto=(bool v) { lightGroup.setProp("Show3DOnPhoto", Bool(v)); return v; }
This is the most simple way to replace the fields, since no code has to be rewritten if the light group props are made accessible as though they still were fields in the class ...
The Changed Event
/** * Light group changed (quick update). Responsibility of light snappers. */ public void lightGroupChanged(str key, Object value) { if (value as Number) { double v = value.double(); if (key == "intensity") { spotLightIntensity = v; } else if (key == "effectRange") { effectRange = v; } else if (key == "quality") { lightQuality = v; //space.invalidateLight3D(this, #"quality", getLight3D()); // not visible in realtime } else if (key == "shadowSoftness") { if (v == 0 xor shadowSoftness == 0) { shadowSoftness = v; } else { shadowSoftness = v; } } else if (key == "shadowResolution") { shadowResolution = v; } else if (key == "temperature") { temperature = v; } else if (key == "spotConeAnge") { coneAngle = angle(v/2); } else if (key == "fixtureWidth" or key == "fixtureDepth") { // ignore } else { ptrace("TODO (Number): ", key; value); } } else if (value as Bool) { bool v = value.v; if (key == "shadows") { shadows = v; light._useShadows = shadows; } else if (key == "Show3DOnPhoto") { show3DOnPhoto = v; invalidate3D(); } else if (key == "Show2DOnPrint") { show2DOnPrint = v; invalidate2D(dirty2D.all); build2D(); invalidateViewportSnappersInWorldIfNecessary(activeWorld()); } else { ptrace("TODO (Bool): ", key; value); } } else { ptrace("TODO : Class=", value ? value.class : null); } }
Easy enough. You can almost take the code from quick props as it is. Almost.
Clean out from Quick Props
/** * Build quick connectors. */ public bool buildQuickProperties(QuickProperties props) { props.append("on", $spotLightOn, on, on.domain); appendElevation(props); appendTargetHeight(props); props.append(ButtonQuickProperty("settingsDialog", $LightSettingsDialog, "...", null)); return true; } /** * Append elevation quick property. */ final public void appendElevation(QuickProperties props) { props.append("elevation", $lightElevation, pos.z.distance, elevationDomain); } /** * Append target height quick property. */ final public void appendTargetHeight(QuickProperties props) { props.append("targetHeight", $targetHeight, toSpace(targetPoint).z.distance, targetHeightDomain); } /** * Quick property changed. */ public bool quickPropertyChanged(QuickProperties props, str key, Object value, bool testChangeOnly) { if (!testChangeOnly) beforeQuickModify(bsp=false, invalidate=false); if (key == "on") { on(value in Bool and value.Bool.v); } else if (key == "elevation") { if (!testChangeOnly) beforeQuickModify(undo=false, bsp=true, invalidate=false); double elevation = value.Distance.v; double spz = round(elevation, 3); spz = apLockedDistance("loc", spz.distance); double pz = spz - pos.z; targetPoint.z -= pz; setPos((pos.x, pos.y, spz)); apSetDistance("loc", spz.distance); space.invalidateLight3D(this, #"stretch", getLight3D()); invalidate(); space.updateConnectors(this); if (!testChangeOnly) afterQuickModify(undo=false, bsp=true, invalidate=false); } else if (key == "targetHeight") { double targetElev = value.Distance.v; double tpz = round(targetElev, 3); tpz = apLockedDistance("tgt", tpz.distance); targetPoint.z = toSnapper((targetPoint.x, targetPoint.y, tpz)).z; apSetDistance("tgt", tpz.distance); space.invalidateLight3D(this, #"stretch", getLight3D()); invalidate(); space.updateConnectors(this); } else if (key == "settingsDialog") { showLightSettingsDialog(anyFrameWindow); } if (!testChangeOnly) { space.invalidateSelection(); afterQuickModify(bsp=false, invalidate=false); } return true; } /** * Set light on or off. */ final public void on(bool b) { on = b; light.on = on; space.invalidateLight3D(this, #"on", getLight3D()); } /** * Set shadows on or off. */ final public void shadows(bool b) { shadows = b; light._useShadows = shadows; space.invalidateLight3D(this, #"shadows", getLight3D()); }
Light Number Fixes
In addition to the _number
field, you need to implement a few methods ...
/** * Set space. */ public Space space=(Space z) { super(z); renewLightNumberIfNecessary(); return z; } /** * After restore. */ public void afterRestore() { super(); renewLightNumberIfNecessary(); } /** * Renew light number if necessary. */ final public void renewLightNumberIfNecessary() { if (_number == 0) _number = findVacantLightNumber(space, SpotLightSnapper); }
Note that SpotLightSnapper
should be replaced by your own class (and it's not 100% safe to use this.class either as 'this' might be a subclass that you might want to share the number series with, so write it like this, trust me, hehe) ...
/** * Return light snapper identifying number. Different classes have different series, * for instance, we can have Spot Light 1, 2 and 3. And also Grid Light 1 and 2 etc. */ public int lightSnapperNumber() { return _number; }
And finally, we will need a simple hook function ...
/*************************************************************************** * Post load hook ***************************************************************************/ /** * Post load hook. */ private void postLoadHook(World world) { if (world) for (space in world.spaces) { if (space.isAlive) for (z in space.snappersWithLights) { if (z as SpotLightSnapper) z.renewLightNumberIfNecessary(); else if (z as CeilingGridLight) z.renewLightNumberIfNecessary(); else if (z as SunLightSnapper) z.renewLightNumberIfNecessary(); } } } init { putPostLoadWorldHook(function postLoadHook); }
Conclusion
If all of this was done correctly, the end-user should now be able to assign instances of your light snapper to a common group and manipulate all the members of the group at once. It should also be possible to add both spot lights, grid lights, etc, and your own light snapper to a group, where they will share whatever props they have in common.
Comments
0 comments
Please sign in to leave a comment.